如何最小化安卓矢量可绘制文件中的"very long vector path"



如何使用鳄梨优化器工具最小化矢量可绘制文件中的超长矢量路径。

我尝试过使用svg编辑器,但矢量路径仍然很长,所以它收到了非常长的矢量路径警告:

"很长的矢量路径(7985个字符(,这对表演考虑降低精度、删除次要细节或光栅化矢量"在布局资源文件中。

<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="200dp" android:viewportHeight="500">
<path android:fillColor="#e0e0e0" android:pathData="M304,247.48c0.43,-0.67 -1.2,-2.29 -1.21,-3s0.14,-0.3 0.36,-1.46 -1.26,-0.61 -2,-1.72 0.22,-0.64 -0.3,-1.39 -2.34,-0.15 -2.82,-0.53 0,-1.2 -1.08,-1.88 -2.48,0.86 -3.4,0.53 -0.91,-1.06 -1.74,-0.74 -1.65,1.57 -3.38,1.64 -2.34,0.41 -2.34,1.28 -0.07,1.48 -1.67,2.62 -0.89,2.65 -0.13,3.86 -0.83,1.65 -1.06,2.55 1.89,2 2,2.89 -1,2.08 0.82,3.43 1.81,0 2.64,0.75 2.86,2.34 3.59,2a4.68,4.68 0,0 1,2.35 -0.51c0.53,0.15 2.83,0.06 3,-1.06s1.48,-1.57 2.3,-1.47 0.9,-2.13 1.07,-2.81 1.34,-0.38 2.05,-1.3 -0.33,-1.79 -0.23,-2.23S303.52,248.15 304,247.48ZM298.79,248.74c-0.84,0.39 -0.75,0.8 -0.8,1.72s-1.4,1.79 -1.7,1.51 -0.76,0.1 -1.17,0.78 -1.77,1.3 -2.22,0.53 -0.9,-0.69 -1.8,-0.76a1.48,1.48 0,0 1,-1.43 -1.47c0,-0.71 0.3,-0.52 -1.06,-1.52s-1.13,-1.47 -0.75,-2a2.19,2.19 0,0 0,0.38 -2c-0.23,-1 0.27,-1.67 1.25,-1.62s0.65,-1.1 1.11,-1.5 0.65,0.2 1.7,-0.25 1,0.74 2.11,0.75 1.21,-0.88 1.88,-0.44 0,1.33 1.11,1.75 1.31,0.31 1.38,1.08 -0.45,1.7 0,2.11S299.58,248.34 298.74,248.74Z"/>
</vector>

有人能帮我安装鳄梨向量优化器工具吗。

如果您的矢量可绘制标记正确有效,您可以尝试此
基于java脚本的svg优化器

/**
* decompose shorthands to "longhand" commands:
* H, V, S, T => L, L, C, Q 
* reversed method: pathDataToShorthands()
*/
function pathDataToLonghands(pathData) {
pathData = pathDataToAbsolute(pathData);
let pathDataLonghand = [];
let comPrev = {
type: 'M',
values: pathData[0].values
};
pathDataLonghand.push(comPrev)
for (let i = 1; i < pathData.length; i++) {
let com = pathData[i];
let type = com.type;
let values = com.values;
let valuesL = values.length;
let valuesPrev = comPrev.values;
let valuesPrevL = valuesPrev.length;
let [x, y] = [values[valuesL - 2], values[valuesL - 1]];
let cp1X, cp1Y, cp2X, cp2Y;
let [prevX, prevY] = [valuesPrev[valuesPrevL - 2], valuesPrev[valuesPrevL - 1]];
switch (type) {
case 'H':
comPrev = {
type: "L",
values: [values[0], prevY]
}
break;
case 'V':
comPrev = {
type: "L",
values: [prevX, values[0]]
}
break;
case 'T':
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[prevX, prevY] = [valuesPrev[valuesPrevL - 2], valuesPrev[valuesPrevL - 1]];
// new control point
cpN1X = prevX + (prevX - cp1X);
cpN1Y = prevY + (prevY - cp1Y);
comPrev = {
type: "Q",
values: [cpN1X, cpN1Y, x, y]
}
break;
case 'S':
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[cp2X, cp2Y] = valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];
[prevX, prevY] = [valuesPrev[valuesPrevL - 2], valuesPrev[valuesPrevL - 1]];
// new control points
cpN1X = 2 * prevX - cp2X;
cpN1Y = 2 * prevY - cp2Y;
cpN2X = values[0];
cpN2Y = values[1];
comPrev = {
type: "C",
values: [cpN1X, cpN1Y, cpN2X, cpN2Y, x, y]
};
break;
default:
comPrev = {
type: type,
values: values
}
}
pathDataLonghand.push(comPrev);
}
return pathDataLonghand;
}

/**
* apply shorthand commands if possible 
* L, L, C, Q => H, V, S, T 
* reversed method: pathDataToLonghands()
*/
function pathDataToShorthands(pathData, decimals = -1) {
pathData = pathDataToAbsolute(pathData, decimals);
let comShort = {
type: 'M',
values: pathData[0].values
};
let pathDataShorts = [comShort];
for (let i = 1; i < pathData.length; i++) {
let com = pathData[i];
let comPrev = pathData[i - 1];
let type = com.type;
let values = com.values;
let valuesL = values.length;
let valuesPrev = comPrev.values;
let valuesPrevL = valuesPrev.length;
let [x, y] = [values[valuesL - 2], values[valuesL - 1]];
let cp1X, cp1Y, cp2X, cp2Y;
let [prevX, prevY] = [valuesPrev[valuesPrevL - 2], valuesPrev[valuesPrevL - 1]];
let val0R, cpN1XR, val1R, cpN1YR, prevXR, prevYR;
switch (type) {
case 'L':
[val0R, prevXR, val1R, prevYR] = [values[0], prevX, values[1], prevY].map(val => {
return +(val * 2).toFixed(0)
});
if (prevYR == val1R && prevXR !== val0R) {
comShort = {
type: "H",
values: [values[0]]
}
} else if (prevXR == val0R && prevYR !== val1R) {
comShort = {
type: "V",
values: [values[1]]
}
} else {
comShort = com;
}
break;
case 'Q':
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[prevX, prevY] = [valuesPrev[valuesPrevL - 2], valuesPrev[valuesPrevL - 1]];
// Q control point
cpN1X = prevX + (prevX - cp1X);
cpN1Y = prevY + (prevY - cp1Y);
// control points can be reflected
// rounded values for better tolerance
[val0R, cpN1XR, val1R, cpN1YR] = [values[0], cpN1X, values[1], cpN1Y].map(val => {
return +(val * 2).toFixed(0)
});
if (val0R == cpN1XR && val1R == cpN1YR) {
comShort = {
type: "T",
values: [x, y]
}
} else {
comShort = com;
}
break;
case 'C':
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[cp2X, cp2Y] = valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];
[prevX, prevY] = [valuesPrev[valuesPrevL - 2], valuesPrev[valuesPrevL - 1]];
// C control points
cpN1X = 2 * prevX - cp2X;
cpN1Y = 2 * prevY - cp2Y;
cpN2X = values[2];
cpN2Y = values[3];
// control points can be reflected
// rounded values for better tolerance
[val0R, cpN1XR, val1R, cpN1YR] = [values[0], cpN1X, values[1], cpN1Y].map(val => {
return +(val * 2).toFixed(0)
});
if (val0R == cpN1XR && val1R == cpN1YR) {
comShort = {
type: "S",
values: [cpN2X, cpN2Y, x, y]
};
} else {
comShort = com
}
break;
default:
comShort = {
type: type,
values: values
}
}
pathDataShorts.push(comShort);
}
return pathDataShorts;
}

/**
* dependancy: Jarek Foks's pathdata polyfill
* cdn: https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.4/path-data-polyfill.min.js
* github: https://github.com/jarek-foksa/path-data-polyfill
* gist: https://gist.github.com/herrstrietzel/1d8c2871436463fe0b4ce5ffaee4d2be
**/
// convert to relative commands
function pathDataToRelative(pathData, decimals = -1, unlink = false) {
// remove object reference
pathData = unlink ? JSON.parse(JSON.stringify(pathData)) : pathData;
let M = pathData[0].values;
let x = M[0],
y = M[1],
mx = x,
my = y;
// loop through commands
for (let i = 1; i < pathData.length; i++) {
let cmd = pathData[i];
let type = cmd.type;
let typeRel = type.toLowerCase();
let values = cmd.values;
// is absolute
if (type != typeRel) {
type = typeRel;
cmd.type = type;
// check current command types
switch (typeRel) {
case "a":
values[5] = +(values[5] - x);
values[6] = +(values[6] - y);
break;
case "v":
values[0] = +(values[0] - y);
break;
case "m":
mx = values[0];
my = values[1];
default:
// other commands
if (values.length) {
for (let v = 0; v < values.length; v++) {
// even value indices are y coordinates
values[v] = values[v] - (v % 2 ? y : x);
}
}
}
}
// is already relative
else {
if (cmd.type == "m") {
mx = values[0] + x;
my = values[1] + y;
}
}
let vLen = values.length;
switch (type) {
case "z":
x = mx;
y = my;
break;
case "h":
x += values[vLen - 1];
break;
case "v":
y += values[vLen - 1];
break;
default:
x += values[vLen - 2];
y += values[vLen - 1];
}
// round coordinates
if (decimals >= 0) {
cmd.values = values.map((val) => {
return +val.toFixed(decimals);
});
}
}
// round M (starting point)
if (decimals >= 0) {
[M[0], M[1]] = [+M[0].toFixed(decimals), +M[1].toFixed(decimals)];
}
return pathData;
}
function pathDataToAbsolute(pathData, decimals = -1, unlink = false) {
// remove object reference
pathData = unlink ? JSON.parse(JSON.stringify(pathData)) : pathData;
let M = pathData[0].values;
let x = M[0],
y = M[1],
mx = x,
my = y;
// loop through commands
for (let i = 1; i < pathData.length; i++) {
let cmd = pathData[i];
let type = cmd.type;
let typeAbs = type.toUpperCase();
let values = cmd.values;
if (type != typeAbs) {
type = typeAbs;
cmd.type = type;
// check current command types
switch (typeAbs) {
case "A":
values[5] = +(values[5] + x);
values[6] = +(values[6] + y);
break;
case "V":
values[0] = +(values[0] + y);
break;
case "H":
values[0] = +(values[0] + x);
break;
case "M":
mx = +values[0] + x;
my = +values[1] + y;
default:
// other commands
if (values.length) {
for (let v = 0; v < values.length; v++) {
// even value indices are y coordinates
values[v] = values[v] + (v % 2 ? y : x);
}
}
}
}
// is already absolute
let vLen = values.length;
switch (type) {
case "Z":
x = +mx;
y = +my;
break;
case "H":
x = values[0];
break;
case "V":
y = values[0];
break;
case "M":
mx = values[vLen - 2];
my = values[vLen - 1];
default:
x = values[vLen - 2];
y = values[vLen - 1];
}
// round coordinates
if (decimals >= 0) {
cmd.values = values.map((val) => {
return +val.toFixed(decimals);
});
}
}
// round M (starting point)
if (decimals >= 0) {
[M[0], M[1]] = [+M[0].toFixed(decimals), +M[1].toFixed(decimals)];
}
return pathData;
}

function setPathDataOpt(path, pathData, decimals) {
let d = '';
pathData.forEach((com, c) => {
let type = com["type"];
let values = com["values"];
if (decimals >= 0) {
values.forEach(function(val, v) {
pathData[c]["values"][v] = +val.toFixed(decimals);
});
}
d += `${type}${values.join(" ")}`;
});
d = d
.replace(/ss+/g, ' ', '')
.replaceAll(",", " ")
.replaceAll(" -", "-")
path.setAttribute("d", d);
}
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
}
svg {
height: 20em;
width: auto;
max-width: 100%;
margin: 0.3em;
overflow: visible;
border: 1px solid #ccc;
}
textarea {
width: 100%;
min-height: 30em;
font-family: monospace;
white-space: pre-wrap;
}
.flex {
display: flex;
gap: 1em;
}
.col {
flex: 1 1 auto;
width: 100%;
}
<label>Round to n decimals<input id="round" class="inputs" type="number" min="-1" max="8" value="1"></label>
<label><input name="absoluteRelative" class="inputs absoluteRelative" type="radio" min="-1" max="8"
value="absolute"> Absolute </label>
<label><input name="absoluteRelative" class="inputs absoluteRelative" type="radio" min="-1" max="8" value="relative"
checked> Relative </label>
<label> <input id="shorthands" class="inputs" type="checkbox" value="1" checked> Apply shorthands</label>
<label> <input id="crop" class="inputs" type="checkbox" value="1" checked> Crop and center</label>
<div class="flex">
<div class="col">
<h3>Vector drawable input</h3>
<textarea id="svgIn" class="inputs filesize">
<vector xmlns:android="http://schemas.android.com/apk/res/android" 
android:width="200dp" 
android:height="200dp" 
android:viewportWidth="500" 
android:viewportHeight="500">
<path android:fillColor="#e0e0e0" android:pathData="M304,247.48c0.43,-0.67 -1.2,-2.29 -1.21,-3s0.14,-0.3 0.36,-1.46 -1.26,-0.61 -2,-1.72 0.22,-0.64 -0.3,-1.39 -2.34,-0.15 -2.82,-0.53 0,-1.2 -1.08,-1.88 -2.48,0.86 -3.4,0.53 -0.91,-1.06 -1.74,-0.74 -1.65,1.57 -3.38,1.64 -2.34,0.41 -2.34,1.28 -0.07,1.48 -1.67,2.62 -0.89,2.65 -0.13,3.86 -0.83,1.65 -1.06,2.55 1.89,2 2,2.89 -1,2.08 0.82,3.43 1.81,0 2.64,0.75 2.86,2.34 3.59,2a4.68,4.68 0,0 1,2.35 -0.51c0.53,0.15 2.83,0.06 3,-1.06s1.48,-1.57 2.3,-1.47 0.9,-2.13 1.07,-2.81 1.34,-0.38 2.05,-1.3 -0.33,-1.79 -0.23,-2.23S303.52,248.15 304,247.48ZM298.79,248.74c-0.84,0.39 -0.75,0.8 -0.8,1.72s-1.4,1.79 -1.7,1.51 -0.76,0.1 -1.17,0.78 -1.77,1.3 -2.22,0.53 -0.9,-0.69 -1.8,-0.76a1.48,1.48 0,0 1,-1.43 -1.47c0,-0.71 0.3,-0.52 -1.06,-1.52s-1.13,-1.47 -0.75,-2a2.19,2.19 0,0 0,0.38 -2c-0.23,-1 0.27,-1.67 1.25,-1.62s0.65,-1.1 1.11,-1.5 0.65,0.2 1.7,-0.25 1,0.74 2.11,0.75 1.21,-0.88 1.88,-0.44 0,1.33 1.11,1.75 1.31,0.31 1.38,1.08 -0.45,1.7 0,2.11S299.58,248.34 298.74,248.74Z"/>
</vector>
</textarea>
<svg id="svgPreview">
<path id="pathPrev" />
</svg>
</div>
<div class="col">
<h3>Vector drawable: relative, shorthands, rounded</h3>
<textarea id="svgOut" class="filesize"></textarea>
<p id="pathSize"></p>
<svg id="svgNew">
<path id="pathNew" />
</svg>
</div>
<div id="vectorDrawable"></div>
</div>

<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@latest/path-data-polyfill.min.js"></script>
<script>
let decimals = +round.value;
let inputs = document.querySelectorAll('.inputs');
inputs.forEach(input => {
input.addEventListener('input', e => {
updateSVG()
})
})
window.addEventListener('DOMContentLoaded', e => {
updateSVG();
})

function updateSVG() {
let useRelative = document.querySelector('.absoluteRelative:checked').value;
decimals = +round.value;
let svgCode = cleanStr(svgIn.value);
//drawable to svg
let ns = 'http://schemas.android.com/apk/res/android';
let xml = new DOMParser();
let doc = xml.parseFromString(svgCode, "application/xml");
let vector = doc.querySelector('vector');
vectorDrawable.appendChild(vector);
let vectorPath = vector.querySelector('path');
let vectorD = vectorPath.getAttribute('android:pathData');
// apply path data to preview svg
pathPrev.setAttribute('d', vectorD);
let pathData = pathPrev.getPathData();
let viewportWidth = vector.getAttribute('android:viewportWidth');
let viewportHeight = vector.getAttribute('android:viewportHeight');
svgPreview.setAttribute('viewBox', [0, 0, viewportWidth, viewportHeight].join(' '));

// crop and center icon
let cropAndCenter = crop.checked ? true : false;
if (cropAndCenter) {
let bb = pathPrev.getBBox();
let [x, y, width, height] = [bb.x, bb.y, bb.width, bb.height];
// apply rounded viewport height divisable by 4
viewportHeight = Math.ceil(height / 4) * 4;
viewportWidth = Math.ceil(width / 4) * 4;
// add necessary attributes
vector.setAttributeNS(ns, 'android:viewportWidth', viewportWidth);
vector.setAttributeNS(ns, 'android:viewportHeight', viewportHeight);
vector.setAttributeNS(ns, 'android:width', viewportWidth + 'dp');
vector.setAttributeNS(ns, 'android:height', viewportHeight + 'dp');
pathData = pathDataToRelative(pathData);
let xN = pathData[0].values[0] - x;
let yN = pathData[0].values[1] - y;
let offsetX = (viewportWidth - width) / 2;
let offsetY = (viewportHeight - height) / 2;
pathData[0].values = [xN + offsetX, yN + offsetY]
}

let useShorthands = shorthands.checked ? true : false;
if (useShorthands) {
pathData = pathDataToShorthands(pathData, decimals);
} else {
pathData = pathDataToLonghands(pathData, decimals);
}
if (useRelative === 'relative') {
pathData = pathDataToRelative(pathData);
} else {
pathData = pathDataToAbsolute(pathData);
}
setPathDataOpt(pathNew, pathData, decimals)
let dMin = pathNew.getAttribute('d');
// update drawable
vectorPath.setAttributeNS(ns, 'android:pathData', dMin);
let serializer = new XMLSerializer();
let xmlStr = serializer.serializeToString(vector).replace(/>s+/g, '>').replaceAll('><', '>n<');
pathSize.textContent = filesize(dMin) + ' Bytes (pathdata)';
svgOut.value = xmlStr;
svgNew.setAttribute('viewBox', [0, 0, viewportWidth, viewportHeight].join(' '));
showFileSize();
}
function showFileSize() {
let textareas = document.querySelectorAll('.filesize');
textareas.forEach(text => {
let pSize = text.nextElementSibling;
if (!pSize.classList.contains('pFileSize')) {
pSize = document.createElement('p');
pSize.classList.add('pFileSize');
text.parentNode.insertBefore(pSize, text.nextElementSibling);
}
let size = new Blob([text.value]).size;
size = +size.toFixed(3);
pSize.textContent = size + ' Bytes'
})
}
function filesize(str) {
let size = new Blob([str]).size;
return +size.toFixed(3);
}
function cleanStr(str) {
str = str.
replace(/[(trn]+/g, '').
replace(/s{2,}/g, ' ').
trim();
return str;
}
function adjustViewBox(svg) {
let bb = svg.getBBox();
let bbVals = [bb.x, bb.y, bb.width, bb.height].map((val) => {
return +val.toFixed(2);
});
let maxBB = Math.max(...bbVals);
let [x, y, width, height] = bbVals;
svg.setAttribute("viewBox", [x, y, width, height].join(" "));
}
</script>

Codepen示例

它的工作原理

  1. 向量可绘制通过DOMParser()附加到DOM
  2. 将CCD_ 2转换为CCD_
  3. svg-pathData使用getPathData()解析(需要Jarek-Foksa的polyfill(
  4. 路径数据的缩小方式为:
    4.1将坐标四舍五入到n小数精度
    4.2命令转换为相对命令(自定义辅助对象方法pathDataToRelative(pathData)(
    4.3命令转换为简写命令(如果适用,通过pathDataToShorthands()辅助对象(
  5. 可选裁剪和居中到合适的viewPort
  6. 优化后的CCD_ 8数据应用于CCD_

显然,Android Studio中矢量可绘制元素的最大字节大小是800字节,如本文所述:"超长矢量路径"问题…以及在哪里可以找到它们。

如果你不能把它减少到<800字节通过上述优化:您需要在矢量编辑器中重构<path>,如inkscape、Adobe Illusrator等。例如,通过使用简化函数或手动删除不必要的点。

相关内容

最新更新