preface

I wrote several advanced UI series articles in front of me, and I feel good. Due to the content of the work, the author has little knowledge about UI development, so I plan to write some articles about UI, which can also be regarded as a comprehensive review for myself. This article is still basic concept + actual combat to explain.

concept

Scalable Vector Graphics (SVG) is a Scalable Vector Graphics standard for networks. The corresponding to Vector Graphics are bitmaps, which are made up of pixels. When the picture is enlarged to a certain size, there will be Mosaic phenomenon, Photoshop is commonly used bitmap processing software, and vector map is composed of a point, after mathematical calculation using straight lines and curves drawn, no matter how to enlarge, there will be no Mosaic problem, Illustrator is commonly used vector drawing software.

SVG VS Bitmap

Benefits:

  1. SVG defines graphics in AN XML format that can be read and modified by a wide variety of tools;
  2. SVG is stored by points and drawn by computer according to point information without distortion. There is no need to adapt multiple sets of ICONS according to resolution.
  3. SVG takes up less space than Bitmap. For example, a 500px by 500px image takes up 20KB when converted to SVG, while a PNG image takes up 732KB.
  4. SVG can transform Path paths, which can be combined with Path animations to create richer animations.

The vector label

In Android, SVG vectors are defined using tags and stored in the res/drawable/ directory. A simple SVG image code is defined as follows:

<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
        android:height="24dp"
        android:viewportHeight="1024" 
        android:viewportWidth="1024" 
        android:width="24dp"
        tools:ignore="MissingDefaultResource">
    <path android:fillColor="# 040000" 
          android:pathData="M513.29, 738 h - 2.3 V0h2. 3 z"/>
    <path android:fillColor="# 040000"
          android:pathData="M512.08, S482.38 727.97, 896.04, 480.09, 939.08-0.76 c, 14.31-9.58, 84.92, 32.88, 84.92,"/>
    <path android:fillColor="# 040000"
          android:pathData="M511.02, 1024 c42. 47, 0, 33.66, 70.6, 32.89-84.92-2.3-43.04-31.99, 211.11-31.99, 211.11,"/>
</vector>
Copy the code

It defines the image as follows:

In this code, we first use the vector tag to specify that this is an SVG image, which has the following properties.

  • Width /height: indicates the width and height of the SVG
  • ViewportHeight/viewportWidth: shows the proportion of SVG graphics division

The path tag

Commonly used attributes

Tag name instructions
android:name Declare a tag, similar to an ID, to make it easy to find the node when animating it
android:pathData Description of SVG vector diagrams
android:strokeWidth Brush width
android:fillColor Fill color
android:fillAlpha The opacity of the fill color
android:strokeColor Stroke color
android:strokeWidth Stroke width
android:strokeAlpha Stroke transparency
android:strokeLineJoin Used to specify the shape of a broken line corner, including miter (the junction is an acute Angle), round(the junction is an arc), bevel(the junction is a straight line)
android:strokeLineCap Draw the shape of the end of the line (butt, round, square).
android:strokeMiterLimit Set the upper limit of the bevel

Android: trimPathStart properties

This attribute is used to specify where the path starts. The value ranges from 0 to 1, representing the percentage of the path start position. If the value is 0, it starts from the header. When the value is 1, the entire path is invisible.

Android: trimPathEnd properties

This attribute is used to specify the end location of a path. The value ranges from 0 to 1, indicating the percentage of the end location of a path. When the value is 1, the path ends normally. When the value is 0, it indicates that the path ends at the beginning and the entire path is invisible.

Android: trimPathOffset properties

This attribute is used to specify the displacement distance of the result path. The value ranges from 0 to 1. When the value is 0, no displacement is carried out. When the value is 1, the length of the entire path is shifted.

Android: pathData properties

In the PATH tag, the display content of an SVG image is specified primarily through the pathData attribute. There is more to the pathData attribute than the initial M and L directives.

instruction The corresponding instructions
M moveto(M x,y) Moves the brush to the specified position
L lineto(L X,Y) Draw a line to the specified coordinates
H Horizontal lineto(H X) Draw a horizontal line to the specified X position
V Vertical lineto(V Y) Draw a vertical line to the specified Y position
C curveto(C X1,Y1,X2,Y2,ENDX,ENDY) Bezier curve of the third order
S Smooth curveto(S X2,Y2,ENDX,ENDY) Bezier curve of the third order
Q Quadratic Belzier curve(Q X,Y,ENDX,ENDY) Bezier curve of second order
T smooth quadratic Belaizer curveto(T ENDX,ENDY) Map the endpoint after the previous path
A elliptic Arc(A RX,RY,XROTATION,FLAYG1,FLAY2,X,Y) arc
Z Closepath Shut down the path

Making SVG images

Method 1: Design software

If you have drawing skills, you can create SVG images using Illustrator or an online SVG tool, such as editor.method.ac/, or download and edit them from an SVG source file download site.

Method 2: Iconfont

Alibaba’s vector gallery

Introducing SVG graphics to Android

The preparatory work

We know that Android does not support direct SVG image parsing. We must convert SVG images into vector tags. There are two ways to do this.

Method 1: Online conversion

Click to jump to online conversion site

Method 2: AS transfer

Follow the steps I did above to generate the Vector image

Based on using

Here is how to use vector directly for ImageView (ps: here is the androidx version, if the lower version needs to do their own compatibility);

  1. Use it in ImageView

    <?xml version="1.0" encoding="utf-8"? >
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                    xmlns:tools="http://schemas.android.com/tools"
                    android:orientation="vertical"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent" tools:ignore="MissingDefaultResource">
    
        <ImageView
                android:id="@+id/iv"
                android:layout_centerInParent="true"
                android:layout_width="match_parent"
                android:src="@drawable/ic_line"
                android:layout_height="500dp"/>
    
    </RelativeLayout>
    Copy the code

Use the advanced

We’ve covered the vector tag, how to display a vector statically, and how to create an SVG image. In this section, we’ll focus on dynamic vectors, which are the essence of SVG images for Android applications.

To implement a Vector animation, we first need the Vector image and its corresponding animation. Here we still use the image of the water drop state in the previous section.

Let’s take a look at the results:

  1. Define a name for path, as shown below

  2. Define an Animator file to animate the Vector image

    <?xml version="1.0" encoding="utf-8"? >
    <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
                    android:propertyName="trimPathStart"
                    android:valueFrom="1"
                    android:valueTo="0"
                    android:duration="3000"
    >
    </objectAnimator>
    Copy the code

    Note that the file corresponds to the path tag in the Vector, and the animation dynamically changes the trimPathStart value of the PATH tag from 0 to 1.

  3. Define moving-vector for association

    <? xml version="1.0" encoding="utf-8"? > <animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
                     xmlns:tools="http://schemas.android.com/tools" android:drawable="@drawable/ic_line"
                     tools:targetApi="lollipop">
    
        <target android:animation="@anim/anim_start"
                android:name="num_1"></target>
        <target android:animation="@anim/anim_start"
                android:name="num_2"></target>
    
        <target android:animation="@anim/anim_start"
                android:name="num_3"></target>
    </animated-vector>
    Copy the code

    In the code above, drawable represents the associated vector image and target represents the path name associated with the animation

  4. Set in code

    class SVGDemo1Activity : AppCompatActivity() {
    
        @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
        override fun onCreate(savedInstanceState: Bundle?). {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_svg)
            startAnimatabe()
        }
    
        private fun startAnimatabe(a) {
            val animatedVectorDrawable = AnimatedVectorDrawableCompat.create(this, R.drawable.line_animated_vector)
            iv.setImageDrawable(animatedVectorDrawable)
            val animatable = iv.drawable as Animatable
            animatable.start()
        }
    }
    Copy the code

In actual combat

Enter search animation

  1. Make search ICONS using an online drawing SVG icon site

    You can do whatever you want with the drawing. Once you’re done, go to View -> Source and copy the SVG code and save it as search_svG.xml

  2. Convert SVg2Vector online

    Click blank or drag the SVG directly to the specified area for conversion

  3. Import the transformed Android vector into AS

  4. Start making animated associations

    //1. Define the animation under /res/aniamator<?xml version="1.0" encoding="utf-8"? >
    <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
                    android:propertyName="trimPathStart"
                    android:valueFrom="1"
                    android:valueTo="0"
                    android:duration="2000"
    >
    </objectAnimator>//2. Define vector at /res/drawable/<?xml version="1.0" encoding="utf-8"? >
    <vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="580dp"
        android:height="400dp"
        android:viewportWidth="580"
        android:viewportHeight="400">
    
    
        <path
            android:name="svg_1"
            android:strokeColor="# 000"
            android:strokeWidth="1.5"
            android:pathData="M 164.54545 211.91761 L 380 212.8267" />
        <path
            android:name="svg_2"
            android:strokeColor="# 000"
            android:strokeWidth="1.5"
            android:pathData="M 360 180.09943 C 366.024924042 180.09943 370.90909 184.780091469 370.90909 190.55398 C 370.90909 196.327868531 366.024924042 201.00853 360 201.00853 C 353.975075958 201.00853 349.09091 196.327868531 349.09091 190.55398 C 349.09091 184.780091469 353.975075958 180.09943 360 180.09943 Z" />
        <path
            android:name="svg_3"
            android:strokeColor="# 000"
            android:strokeWidth="1.5"
            android:pathData="M 369.09091 197.37216 L 380.90909 208.28125" />
    </vector>//3. Associate animation and vector in /res/drawable/<?xml version="1.0" encoding="utf-8"? >
    <animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
                     xmlns:tools="http://schemas.android.com/tools"
                     android:drawable="@drawable/search_svg"
                     tools:targetApi="lollipop">
    
        <target android:animation="@animator/anim_start"
                android:name="svg_1"></target>
        <target android:animation="@animator/anim_start"
                android:name="svg_2"></target>
    
        <target android:animation="@animator/anim_start"
                android:name="svg_3"></target>
    </animated-vector>
    Copy the code
  5. The effect

    Still very dazzle,😁! The code in the making

Police car lights flashing

For details, please go to GitHub

Toutiao pull down refresh animation

For a complex combination animation, see the following:

  1. Prepare vector data

    <vector xmlns:android="http://schemas.android.com/apk/res/android"
            android:width="200dp"
            android:height="200dp"
            android:viewportHeight="200"
            android:viewportWidth="200">
    
        <path
                android:name="tt_1"
                android:fillColor="#C2BFBF"
                android:pathData="M20,30 L100,30 M100,30 L100,90 M100,90 L20,90 M20,90 L20,30"
                android:strokeColor="#C2BFBF"
                android:strokeLineCap="round"
                android:strokeWidth="6"/>
        <path
                android:name="tt_2"
                android:pathData="M120,30 L180,30 M120,60 L180,60 M120,90 L180,90"
                android:strokeColor="#C2BFBF"
                android:strokeLineCap="round"
                android:strokeWidth="6"/>
        <path
                android:name="tt_3"
                android:pathData="M20,120 L180,120 M20,150 L180,150 M20,180 L180,180"
                android:strokeColor="#C2BFBF"
                android:strokeLineCap="round"
                android:strokeWidth="6"/>
    
        <path
                android:pathData="M0,0 L200,0 M200,0 L200,200 M200,200 L0,200 M0,200 L0,0
                android:strokeColor="#C2BFBF"
                android:strokeLineCap="round"
                android:strokeWidth="6"/>
    </vector>
    Copy the code
  2. Define executing the animation clockwise and doing the pathData transform

    Here’s an example of a position change:

    <?xml version="1.0" encoding="utf-8"? >
    <set xmlns:android="http://schemas.android.com/apk/res/android"
         
         android:ordering="sequentially">// Perform the pathData position transformation in sequence<objectAnimator 
                android:duration="600"
                android:interpolator="@android:interpolator/decelerate_cubic"
                android:propertyName="pathData"
                android:valueFrom="M20,30 L100,30 M100,30 L100,90 M100,90 L20,90 M20,90 L20,30"
                android:valueTo=M100,30 L180,30 M180,30 L180,90 M180,90 L100,90 M100,90 L100,30"
                android:valueType="pathType" />
        <objectAnimator
               
                android:duration="600"
                android:interpolator="@android:interpolator/decelerate_cubic"
                android:propertyName="pathData"
                android:valueFrom=M100,30 L180,30 M180,30 L180,90 M180,90 L100,90 M100,90 L100,30"
                android:valueTo="M100,120 L180,120 M180,120 L180,180 M180,180 L100,180 M100,180 L100,120"
                android:valueType="pathType" />
        <objectAnimator
                
                android:duration="600"
                android:interpolator="@android:interpolator/decelerate_cubic"
                android:propertyName="pathData"
                android:valueFrom="M100,120 L180,120 M180,120 L180,180 M180,180 L100,180 M100,180 L100,120"
                android:valueTo="M20,120 L100,120 M100,120 L100,180 M100,180 L20,180 M20,180 L20,120"
                android:valueType="pathType" />
        <objectAnimator
                
                android:duration="600"
                android:interpolator="@android:interpolator/decelerate_cubic"
                android:propertyName="pathData"
                android:valueFrom="M20,120 L100,120 M100,120 L100,180 M100,180 L20,180 M20,180 L20,120"
                android:valueTo="M20,30 L100,30 M100,30 L100,90 M100,90 L20,90 M20,90 L20,30"
                android:valueType="pathType" />
    </set>
    Copy the code

    Take a look at the path tag in this article if you don’t know what it means. If you don’t understand the label, you don’t understand it at all.

  3. To associate

    <?xml version="1.0" encoding="utf-8"? >
    <animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
                     xmlns:tools="http://schemas.android.com/tools"
                     android:drawable="@drawable/ic_toutiao"
    
                     tools:targetApi="lollipop">
    
    
        <target
                android:animation="@animator/tt_path_one"
                android:name="tt_1"/>
    
        <target
                android:animation="@animator/tt_path_two"
                android:name="tt_2"/>
    
        <target
                android:animation="@animator/tt_path_three"
                android:name="tt_3"/>
    
    
    </animated-vector>
    Copy the code
  4. Code controls repeated execution

    class SVGDemo1Activity : AppCompatActivity() {
    
    
        var reStartTT = @SuppressLint("HandlerLeak")
        object : Handler() {
            override fun handleMessage(msg: Message) {
                super.handleMessage(msg)
                startAnimatabe(R.drawable.line_animated_toutiao, true)}}@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
        override fun onCreate(savedInstanceState: Bundle?). {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_svg)
    
            // Drop animation
            startWaterDropAnimator.setOnClickListener {
                startAnimatabe(R.drawable.line_animated_vector, false)}// Search animation
            startSearchAnimator.setOnClickListener {
                startAnimatabe(R.drawable.line_animated_search, false)}// Execute the police car animation
            startPoliceCarAnimator.setOnClickListener {
                startAnimatabe(R.drawable.line_animated_car, false)}// Execute the headline animation
            startTTAnimator.setOnClickListener {
                startAnimatabe(R.drawable.line_animated_toutiao, true)}}private fun startAnimatabe(lineAnimatedVector: Int, isRegister: Boolean): Animatable {
            val animatedVectorDrawable = AnimatedVectorDrawableCompat.create(this, lineAnimatedVector)
            iv.setImageDrawable(animatedVectorDrawable)
            val animatable = iv.drawable asAnimatable animatable.start() animatedVectorDrawable!! .registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
                override fun onAnimationEnd(drawable: Drawable?). {
                    super.onAnimationEnd(drawable)
                    if(! isRegister)return
                    animatedVectorDrawable.unregisterAnimationCallback(this)
                    // Restart in XML setting restart invalid temporarily implemented with Handler.
                    reStartTT.sendEmptyMessage(0)}})return animatable
    
        }
    }
    Copy the code

    For details, please go to GitHub

Mapping China

In this article, SVG pathData is implemented using ImageView. Not all situations are suitable for the above method. For example, IF I want to implement pathData field click, then the above method should not be implemented. Let’s look at an example of how to customize a View to implement PathData and PathData field click events.

Let’s use path to draw a map of China. First, let’s take a look at the final effect, as follows:

Doesn’t it look cool, it looks good, hey, hey, let’s see how to do that.

  1. Prepare map SVG

    • First, download the map data

    • Choose to download free map data

    • Find the corresponding country and click to download SVG data

    • Select the corresponding map data, I downloaded high-quality SVG here

  2. SVG to Vector xml

    Convert the downloaded China-svg file into the XML data of the vector node, or use AS.

    After turning, put it into AS, AS shown below

    Now that we have this data, we can parse the XML path node, get the pathData and we can draw the path. So let’s start parsing the XML. There are many different ways to parse it, but in this case we’ll use dom.

  3. Start parsing the XML

    There are many ways to parse XML, so I’m going to use DOM parsing directly here, PathData2Path I here directly with the Android SDK provides the Android support. The v4. Graphics# PathParser because it has been labeled the hide attribute in the source code, We need to copy it directly into our own project, see the following code for specific transformation:

            /** * start parsing XML */
            public fun dom2xml(stream: InputStream?).: MutableList<MapData> {
                mapDataLists.clear()
                //dom
                val newInstance = DocumentBuilderFactory.newInstance()
                val newDocumentBuilder = newInstance.newDocumentBuilder()
                // Get the Docment object
                val document = newDocumentBuilder.parse(stream)
                // Get all the information in the XML that belongs to the path node
                val elementsByTagName = document.getElementsByTagName(PATH_TAG)
    
                // Define four points to determine the scope of the entire map
                var left = - 1f
                var right = - 1f
                var top = - 1f
                var bottom = - 1f
                // Start iterating through the tags to get the PATH data group
                for (pathData in 0 until elementsByTagName.length) {
                    val item = elementsByTagName.item(pathData) as Element
                    val name = item.getAttribute("android:name")
                    val fillColor = item.getAttribute("android:fillColor")
                    val strokeColor = item.getAttribute("android:strokeColor")
                    val strokeWidth = item.getAttribute("android:strokeWidth")
                    val pathData = item.getAttribute("android:pathData")
                    val path = PathParser.createPathFromPathData(pathData)
                    mapDataLists.add(MapData(name, fillColor, strokeColor, strokeWidth, path))
                    // Get the width of the control
                    val rect = RectF()
                    // Get the boundaries of each province
                    path.computeBounds(rect, true)
                    // Select left from each path and select all minimum values
                    left = if (left == - 1f) rect.left else Math.min(left, rect.left)
                    // Take all the maximum values of the right in each path
                    right = if (right == - 1f) rect.right else Math.max(right, rect.right)
                    // Select top from each path and select all minimum values
                    top = if (top == - 1f) rect.top else Math.min(top, rect.top)
                    // Iterate over the bottom in each path to get all the maximum values
                    bottom = if (bottom == - 1f) rect.bottom else Math.max(bottom, rect.bottom)
                }
                // The rectangle of the MAP
                MAP_RECTF = RectF(left, top, right, bottom)
                return mapDataLists;
            }
    Copy the code
  4. Control measurement ADAPTS to vertical and horizontal switching and width and height define WRAP_content mode

        /** * start measuring */
        override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
            // Measurement mode
            var widthMode = MeasureSpec.getMode(widthMeasureSpec)
            var heightMode = MeasureSpec.getMode(heightMeasureSpec)
            // Measure the size
            widthSize = MeasureSpec.getSize(widthMeasureSpec)
            heightSize = MeasureSpec.getSize(heightMeasureSpec)
    
            if(! MAP_RECTF.isEmpty && mMapRectHeight ! =0f && mMapRectWidth ! =0f) {
                // Display the scale
                scaleHeightValues = heightSize / mMapRectHeight
                scaleWidthValues = widthSize / mMapRectWidth
            }
    
            // The width and height of the XML file wrap_content
            if (widthMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.AT_MOST) {
                // If the width of the horizontal screen is reserved maximum, the height needs to be adapted
                if(widthSize < heightSize && mMapRectHeight ! =0f) {
                    setMeasuredDimension(widthSize, (mMapRectHeight * scaleWidthValues).toInt())
                } else {
                    setMeasuredDimension(widthSize, heightSize)
                }
            } else {
                setMeasuredDimension(widthSize, heightSize)
            }
        }
    Copy the code
  5. Start drawing path

        /** * Draw Map data */
        @SuppressLint("Range")
        private fun drawMap(canvas: Canvas) {
            canvas.save()
            if (widthSize > heightSize) {
                canvas.scale(scaleWidthValues, scaleHeightValues)
            } else {
                canvas.scale(scaleWidthValues, scaleWidthValues)
            }
    
            mapDataList.forEach { data ->
                run {
                    if (data.isSelect) {
                        drawPath(data, canvas, Color.RED)
                    } else {
                        drawPath(data, canvas, Color.parseColor(data.fillColor))
                    }
                }
            }
            canvas.restore()
            canvas.drawText(Map of China 🇨🇳, widthSize / 2 - mPaintTextTitle.measureText(Map of China 🇨🇳) / 2f, 100f, mPaintTextTitle)
        }
    
        /** * start drawing Path */
        private fun drawPath(
            data: MapData,
            canvas: Canvas,
            magenta: Int
        ) {
            mPaintPath.setColor(magenta)
            mPaintPath.setStyle(Paint.Style.FILL)
            mPaintPath.setTextSize(30f)
            mPaintPath.setStrokeWidth(data.strokeWidth.toFloat())
            canvas.drawPath(data.pathData, mPaintPath)
            val rectF = RectF()
            data.pathData.computeBounds(rectF, true)
            canvas.drawText(
                if (data.name.isEmpty()) "" else data.name,
                rectF.centerX() - mPaintText.measureText(data.name) / 2,
                rectF.centerY(), mPaintText
            )
        }
    Copy the code
  6. Add the respective click events to the map

        override fun onTouchEvent(event: MotionEvent): Boolean {
            when (event.action) {
                MotionEvent.ACTION_DOWN -> return true
                MotionEvent.ACTION_UP -> {
                    handlerTouch(event.getX(), event.getY())
                }
            }
            return super.onTouchEvent(event)
        }
    
        /** * handle the click event */
        private fun handlerTouch(x: Float, y: Float) {
            if (mapDataList.size == 0) return
    
            var xScale = 0f
            var yScale = 0f
    
            if (widthSize > heightSize) {
                xScale = scaleWidthValues
                yScale = scaleHeightValues
            } else {
                xScale = scaleWidthValues
                yScale = scaleWidthValues
            }
            mapDataList.forEach { data ->
                run {
                    data.isSelect = false
                    if (isTouchRegion(x / xScale, y / yScale, data.pathData)) {
                        data.isSelect = true
                        postInvalidate()
                    }
                }
            }
        }
    }
    
    /** * Check if it is in the click area */
    fun isTouchRegion(x: Float, y: Float, path: Path): Boolean {
        // Create a rectangle
        val rectF = RectF()
        // Get the rectangle boundary of the current province
        path.computeBounds(rectF, true)
        // Create a region object
        val region = Region()
        // Put the path object into the Region object
        region.setPath(path, Region(rectF.left.toInt(), rectF.top.toInt(), rectF.right.toInt(), rectF.bottom.toInt()))
        // Returns whether the field contains the coordinates passed in
        return region.contains(x.toInt(), y.toInt())
    }
    Copy the code

    See mapView.kt for details

Now that SVG has been explained, you can try drawing maps for other countries.

conclusion

It is important to note here that there are compatibility issues with using SVG on earlier versions, which need to be resolved through separate documentation.

I don’t know if you still remember the last article on advanced UI growth (vi) PathMeasure. In making Path animation, I mentioned that AS long as you give me a Path data, I can draw a graph. After reading this article, do you think there is nothing wrong with what you said? I recommend that you use SVG more often in your projects, as I mentioned at the beginning of this article. SVG graphics and animation effects are all covered here.

tool

  • Create an SVG website online
  • Online SVG to Vector conversion
  • Alibaba iconFont library