cha*_*gam 5 svg minify android-vectordrawable
如何使用鳄梨优化器工具最小化矢量可绘制文件中的非常长的矢量路径。
我尝试过使用 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>
Run Code Online (Sandbox Code Playgroud)
任何人都可以帮助我安装鳄梨矢量优化工具。
如果您的矢量可绘制标记正确且有效,您可以尝试这个
基于java 脚本的 svg 优化器:
/**\n * decompose shorthands to "longhand" commands:\n * H, V, S, T => L, L, C, Q\n * reversed method: pathDataToShorthands()\n */\nfunction pathDataToLonghands(pathData) {\n pathData = pathDataToAbsolute(pathData);\n let pathDataLonghand = [];\n let comPrev = {\n type: "M",\n values: pathData[0].values\n };\n pathDataLonghand.push(comPrev);\n\n for (let i = 1; i < pathData.length; i++) {\n let com = pathData[i];\n let {\n type,\n values\n } = com;\n let valuesL = values.length;\n let valuesPrev = comPrev.values;\n let valuesPrevL = valuesPrev.length;\n let [x, y] = [values[valuesL - 2], values[valuesL - 1]];\n let cp1X, cp1Y, cp2X, cp2Y;\n let [prevX, prevY] = [\n valuesPrev[valuesPrevL - 2],\n valuesPrev[valuesPrevL - 1]\n ];\n switch (type) {\n case "H":\n comPrev = {\n type: "L",\n values: [values[0], prevY]\n };\n break;\n case "V":\n comPrev = {\n type: "L",\n values: [prevX, values[0]]\n };\n break;\n case "T":\n [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];\n [prevX, prevY] = [\n valuesPrev[valuesPrevL - 2],\n valuesPrev[valuesPrevL - 1]\n ];\n // new control point\n cpN1X = prevX + (prevX - cp1X);\n cpN1Y = prevY + (prevY - cp1Y);\n comPrev = {\n type: "Q",\n values: [cpN1X, cpN1Y, x, y]\n };\n break;\n case "S":\n [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];\n [cp2X, cp2Y] =\n valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];\n [prevX, prevY] = [\n valuesPrev[valuesPrevL - 2],\n valuesPrev[valuesPrevL - 1]\n ];\n // new control points\n cpN1X = 2 * prevX - cp2X;\n cpN1Y = 2 * prevY - cp2Y;\n cpN2X = values[0];\n cpN2Y = values[1];\n comPrev = {\n type: "C",\n values: [cpN1X, cpN1Y, cpN2X, cpN2Y, x, y]\n };\n\n break;\n default:\n comPrev = {\n type: type,\n values: values\n };\n }\n pathDataLonghand.push(comPrev);\n }\n return pathDataLonghand;\n}\n\n/**\n * apply shorthand commands if possible\n * L, L, C, Q => H, V, S, T\n */\nfunction pathDataToShorthands(pathData) {\n pathData = pathDataToAbsolute(pathData);\n let comShort = {\n type: "M",\n values: pathData[0].values\n };\n let pathDataShorts = [comShort];\n for (let i = 1; i < pathData.length; i++) {\n let com = pathData[i];\n let comPrev = pathData[i - 1];\n let {\n type,\n values\n } = com;\n let valuesL = values.length;\n let valuesPrev = comPrev.values;\n let valuesPrevL = valuesPrev.length;\n let [x, y] = [values[valuesL - 2], values[valuesL - 1]];\n let cp1X, cp1Y, cp2X, cp2Y;\n let [prevX, prevY] = [\n valuesPrev[valuesPrevL - 2],\n valuesPrev[valuesPrevL - 1]\n ];\n let val0R, cpN1XR, val1R, cpN1YR, cpN1X, cpN1Y, cpN2X, cpN2Y, prevXR, prevYR;\n\n switch (type) {\n case "L":\n [val0R, prevXR, val1R, prevYR] = [\n values[0],\n prevX,\n values[1],\n prevY\n ].map((val) => {\n return +(val * 2).toFixed(1);\n });\n\n if (prevYR == val1R && prevXR !== val0R) {\n comShort = {\n type: "H",\n values: [values[0]]\n };\n } else if (prevXR == val0R && prevYR !== val1R) {\n comShort = {\n type: "V",\n values: [values[1]]\n };\n } else {\n comShort = com;\n }\n break;\n case "Q":\n [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];\n [prevX, prevY] = [\n valuesPrev[valuesPrevL - 2],\n valuesPrev[valuesPrevL - 1]\n ];\n // Q control point\n cpN1X = prevX + (prevX - cp1X);\n cpN1Y = prevY + (prevY - cp1Y);\n\n /**\n * control points can be reflected\n */\n [val0R, cpN1XR, val1R, cpN1YR] = [\n values[0],\n cpN1X,\n values[1],\n cpN1Y\n ].map((val) => {\n return +val.toFixed(1);\n });\n\n if (val0R == cpN1XR && val1R == cpN1YR) {\n comShort = {\n type: "T",\n values: [x, y]\n };\n } else {\n comShort = com;\n }\n break;\n case "C":\n [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];\n [cp2X, cp2Y] =\n valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];\n [prevX, prevY] = [\n valuesPrev[valuesPrevL - 2],\n valuesPrev[valuesPrevL - 1]\n ];\n // C control points\n cpN1X = 2 * prevX - cp2X;\n cpN1Y = 2 * prevY - cp2Y;\n cpN2X = values[2];\n cpN2Y = values[3];\n\n /**\n * control points can be reflected\n */\n [val0R, cpN1XR, val1R, cpN1YR] = [\n values[0],\n cpN1X,\n values[1],\n cpN1Y\n ].map((val) => {\n return +val.toFixed(1);\n });\n\n if (val0R == cpN1XR && val1R == cpN1YR) {\n comShort = {\n type: "S",\n values: [cpN2X, cpN2Y, x, y]\n };\n } else {\n comShort = com;\n }\n break;\n default:\n comShort = {\n type: type,\n values: values\n };\n }\n pathDataShorts.push(comShort);\n }\n return pathDataShorts;\n}\n\n/**\n * dependancy: Jarek Foks\'s pathdata polyfill\n * github: https://github.com/jarek-foksa/path-data-polyfill\n */\nfunction pathDataToRelative(pathData, decimals = -1) {\n let M = pathData[0].values;\n let x = M[0],\n y = M[1],\n mx = x,\n my = y;\n for (let i = 1; i < pathData.length; i++) {\n let cmd = pathData[i];\n let type = cmd.type;\n let typeRel = type.toLowerCase();\n let values = cmd.values;\n\n // is absolute\n if (type != typeRel) {\n type = typeRel;\n cmd.type = type;\n // check current command types\n switch (typeRel) {\n case "a":\n values[5] = +(values[5] - x);\n values[6] = +(values[6] - y);\n break;\n case "v":\n values[0] = +(values[0] - y);\n break;\n case "m":\n mx = values[0];\n my = values[1];\n default:\n // other commands\n if (values.length) {\n for (let v = 0; v < values.length; v++) {\n // even value indices are y coordinates\n values[v] = values[v] - (v % 2 ? y : x);\n }\n }\n }\n }\n // is already relative\n else {\n if (cmd.type == "m") {\n mx = values[0] + x;\n my = values[1] + y;\n }\n }\n let vLen = values.length;\n switch (type) {\n case "z":\n x = mx;\n y = my;\n break;\n case "h":\n x += values[vLen - 1];\n break;\n case "v":\n y += values[vLen - 1];\n break;\n default:\n x += values[vLen - 2];\n y += values[vLen - 1];\n }\n\n // round coordinates\n if (decimals >= 0) {\n cmd.values = values.map((val) => {\n return +val.toFixed(decimals);\n });\n }\n }\n // round M (starting point)\n if (decimals >= 0) {\n [M[0], M[1]] = [+M[0].toFixed(decimals), +M[1].toFixed(decimals)];\n }\n return pathData;\n}\n\nfunction pathDataToAbsolute(pathData, decimals = -1) {\n let M = pathData[0].values;\n let x = M[0],\n y = M[1],\n mx = x,\n my = y;\n\n for (let i = 1; i < pathData.length; i++) {\n let cmd = pathData[i];\n let type = cmd.type;\n let typeAbs = type.toUpperCase();\n let values = cmd.values;\n\n if (type != typeAbs) {\n type = typeAbs;\n cmd.type = type;\n\n switch (typeAbs) {\n case "A":\n values[5] = +(values[5] + x);\n values[6] = +(values[6] + y);\n break;\n\n case "V":\n values[0] = +(values[0] + y);\n break;\n\n case "H":\n values[0] = +(values[0] + x);\n break;\n\n case "M":\n mx = +values[0] + x;\n my = +values[1] + y;\n\n default:\n // other commands\n if (values.length) {\n for (let v = 0; v < values.length; v++) {\n // even value = y coordinates\n values[v] = values[v] + (v % 2 ? y : x);\n }\n }\n }\n }\n // is already absolute\n let vLen = values.length;\n switch (type) {\n case "Z":\n x = +mx;\n y = +my;\n break;\n case "H":\n x = values[0];\n break;\n case "V":\n y = values[0];\n break;\n case "M":\n mx = values[vLen - 2];\n my = values[vLen - 1];\n\n default:\n x = values[vLen - 2];\n y = values[vLen - 1];\n }\n\n // round coordinates\n if (decimals >= 0) {\n cmd.values = values.map((val) => {\n return +val.toFixed(decimals);\n });\n }\n }\n // round M (starting point)\n if (decimals >= 0) {\n [M[0], M[1]] = [+M[0].toFixed(decimals), +M[1].toFixed(decimals)];\n }\n return pathData;\n}\n\nfunction setPathDataOpt(path, pathData, decimals) {\n let d = "";\n pathData.forEach((com, c) => {\n let type = com["type"];\n let values = com["values"];\n\n if (decimals >= 0) {\n values.forEach(function(val, v) {\n pathData[c]["values"][v] = +val.toFixed(decimals);\n });\n }\n d += `${type}${values.join(" ")}`;\n });\n d = d.replace(/\\s\\s+/g, " ", "").replaceAll(",", " ").replaceAll(" -", "-");\n path.setAttribute("d", d);\n}\n\nfunction roundPathData(pathData, decimals = -1) {\n pathData.forEach((com, c) => {\n if (decimals >= 0) {\n com.values.forEach((val, v) => {\n pathData[c].values[v] = +val.toFixed(decimals);\n });\n }\n });\n return pathData;\n}\n\nfunction pathDataToQuadratic(pathData, width, height, precision = 0.1) {\n let newPathData = [pathData[0]];\n for (let i = 1; i < pathData.length; i++) {\n let comPrev = pathData[i - 1];\n let com = pathData[i];\n let [type, values] = [com.type, com.values];\n let [typePrev, valuesPrev] = [comPrev.type, comPrev.values];\n let valuesPrevL = valuesPrev.length;\n let [xPrev, yPrev] = [\n valuesPrev[valuesPrevL - 2],\n valuesPrev[valuesPrevL - 1]\n ];\n\n // convert C to Q\n if (type == "C") {\n let q = cubicToQuad(\n xPrev,\n yPrev,\n values[0],\n values[1],\n values[2],\n values[3],\n values[4],\n values[5],\n precision\n );\n\n let p0 = {\n x: xPrev,\n y: yPrev\n },\n cp1 = {\n x: values[0],\n y: values[1]\n },\n cp2 = {\n x: values[2],\n y: values[3]\n },\n p = {\n x: values[4],\n y: values[5]\n };\n\n /**\n * convert to quadratic\n * if curve is rather flat\n * or shorter than 1/20 width/height of path\n */\n let angle = getAngleABC(cp1, cp2, p);\n let segLength = getDistance(p0, p);\n\n if (segLength < (width + height) / 20 || angle < 20) {\n for (let j = 2; j < q.length; j += 4) {\n newPathData.push({\n type: "Q",\n values: [q[j], q[j + 1], q[j + 2], q[j + 3]].map((val) => {\n return +val.toFixed(9);\n })\n });\n }\n } else {\n newPathData.push(com);\n }\n } else {\n newPathData.push(com);\n }\n }\n return newPathData;\n}\n\n// get distance\nfunction getDistance(p1, p2) {\n if (Array.isArray(p1)) {\n p1.x = p1[0];\n p1.y = p1[1];\n }\n if (Array.isArray(p2)) {\n p2.x = p2[0];\n p2.y = p2[1];\n }\n let [x1, y1, x2, y2] = [p1.x, p1.y, p2.x, p2.y];\n let y = x2 - x1;\n let x = y2 - y1;\n return Math.sqrt(x * x + y * y);\n}\n\n// get angle helper\nfunction getAngle(p1, p2) {\n let angle = (Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180) / Math.PI;\n //console.log(angle);\n return angle;\n}\n\n// get angle between 3 points helper\nfunction getAngleABC(A, B, C) {\n let BA = Math.sqrt(Math.pow(A.x - B.x, 2) + Math.pow(A.y - B.y, 2));\n let AC = Math.sqrt(Math.pow(A.x - C.x, 2) + Math.pow(A.y - C.y, 2));\n let BC = Math.sqrt(Math.pow(C.x - B.x, 2) + Math.pow(C.y - B.y, 2));\n let angle =\n (Math.acos((AC * AC + BA * BA - BC * BC) / (2 * AC * BA)) * 180) / Math.PI;\n return angle;\n}
Run Code Online (Sandbox Code Playgroud)\r\n* {\n box-sizing: border-box;\n}\n\nbody {\n font-family: sans-serif;\n}\n\nsvg {\n height: 20em;\n width: auto;\n max-width: 100%;\n margin: 0.3em;\n overflow: visible;\n border: 1px solid #ccc;\n}\n\ntextarea {\n width: 100%;\n min-height: 30em;\n font-family: monospace;\n white-space: pre-wrap;\n}\n\n.flex {\n display: flex;\n gap: 1em;\n}\n\n.col {\n flex: 1 1 auto;\n width: 100%;\n}
Run Code Online (Sandbox Code Playgroud)\r\n<label>Round to n decimals<input id="round" class="inputs" type="number" min="-1" max="8" value="1"></label>\n<label><input name="absoluteRelative" class="inputs absoluteRelative" type="radio" min="-1" max="8" value="absolute">\n Absolute </label>\n<label><input name="absoluteRelative" class="inputs absoluteRelative" type="radio" min="-1" max="8" value="relative" checked> Relative </label>\n<label> <input id="shorthands" class="inputs" type="checkbox" value="1" checked> Apply shorthands</label>\n<label> <input id="toQuadratic" class="inputs" type="checkbox" value="1">small or flat curves to quadratic</label>\n<label> <input id="crop" class="inputs" type="checkbox" value="1" checked> Crop and center</label>\n\n<div class="flex">\n <div class="col">\n <h3>Vector drawable input</h3>\n <textarea id="svgIn" class="inputs filesize">\n <vector android:height="24dp" android:tint="#FFFFFF"\n android:viewportHeight="24" android:viewportWidth="24"\n android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">\n <path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>\n </vector>\n </textarea>\n <svg id="svgPreview">\n <path id="pathPrev" />\n </svg>\n </div>\n <div class="col">\n <h3>Vector drawable: relative, shorthands, rounded</h3>\n <textarea id="svgOut" class="filesize"></textarea>\n <p id="pathSize"></p>\n <svg id="svgNew">\n <path id="pathNew" />\n </svg>\n </div>\n <div id="vectorDrawable"></div>\n</div>\n\n<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@latest/path-data-polyfill.min.js">\n</script>\n<script src="https://cdn.jsdelivr.net/gh/herrstrietzel/svgHelpers@main/js/pathData.cubic2quad.js">\n</script>\n\n\n\n<script>\n let decimals = +round.value;\n let inputs = document.querySelectorAll(\'.inputs\');\n inputs.forEach(input => {\n input.addEventListener(\'input\', e => {\n updateSVG()\n })\n })\n window.addEventListener(\'DOMContentLoaded\', e => {\n updateSVG();\n })\n\n function updateSVG() {\n let useRelative = document.querySelector(\'.absoluteRelative:checked\').value;\n decimals = +round.value;\n let svgCode = cleanStr(svgIn.value);\n //drawable to svg\n let ns = \'http://schemas.android.com/apk/res/android\';\n let xml = new DOMParser();\n let doc = xml.parseFromString(svgCode, "application/xml");\n let vector = doc.querySelector(\'vector\');\n vectorDrawable.appendChild(vector);\n let vectorPath = vector.querySelector(\'path\');\n let vectorD = vectorPath.getAttribute(\'android:pathData\');\n // apply path data to preview svg\n pathPrev.setAttribute(\'d\', vectorD);\n let pathData = pathPrev.getPathData();\n let viewportWidth = vector.getAttribute(\'android:viewportWidth\');\n let viewportHeight = vector.getAttribute(\'android:viewportHeight\');\n svgPreview.setAttribute(\'viewBox\', [0, 0, viewportWidth, viewportHeight].join(\' \'));\n let {\n x,\n y,\n width,\n height\n } = pathPrev.getBBox();\n // crop and center icon\n let cropAndCenter = crop.checked ? true : false;\n if (cropAndCenter) {\n // apply rounded viewport height divisable by 4\n viewportHeight = Math.ceil(height / 4) * 4;\n viewportWidth = Math.ceil(width / 4) * 4;\n // add necessary attributes\n vector.setAttributeNS(ns, \'android:viewportWidth\', viewportWidth);\n vector.setAttributeNS(ns, \'android:viewportHeight\', viewportHeight);\n vector.setAttributeNS(ns, \'android:width\', viewportWidth + \'dp\');\n vector.setAttributeNS(ns, \'android:height\', viewportHeight + \'dp\');\n pathData = pathDataToRelative(pathData);\n let xN = pathData[0].values[0] - x;\n let yN = pathData[0].values[1] - y;\n let offsetX = (viewportWidth - width) / 2;\n let offsetY = (viewportHeight - height) / 2;\n pathData[0].values = [xN + offsetX, yN + offsetY]\n }\n // pre rounding for low precision\n if (decimals < 2) {\n pathData = roundPathData(pathData, decimals)\n }\n /**\n * cubic curves to quadratic\n */\n let convertToQuad = toQuadratic.checked ? true : false;\n if (convertToQuad) {\n pathData = pathDataToQuadratic(pathDataToLonghands(pathData), width, height);\n }\n let useShorthands = shorthands.checked ? true : false;\n if (useShorthands) {\n pathData = pathDataToShorthands(pathData);\n } else {\n pathData = pathDataToLonghands(pathData);\n }\n if (useRelative === \'relative\') {\n pathData = pathDataToRelative(pathData);\n } else {\n pathData = pathDataToAbsolute(pathData);\n }\n setPathDataOpt(pathNew, pathData, decimals)\n let dMin = pathNew.getAttribute(\'d\');\n // update drawable\n vectorPath.setAttributeNS(ns, \'android:pathData\', dMin);\n let serializer = new XMLSerializer();\n let xmlStr = serializer.serializeToString(vector).replace(/>\\s+/g, \'>\').replaceAll(\'><\
归档时间: |
|
查看次数: |
3794 次 |
最近记录: |