Android简单实现汉字笔顺动画——Java版

在掘金上看到一篇实现汉字笔顺动画的文章很感兴趣,其代码采用kotlin实现,阅读不便,把相关实现类提出来翻译成了Java,有兴趣的可以看下。

先上原文链接:

Android修炼系列(41),简单实现个汉字笔顺动画icon-default.png?t=M4ADhttps://juejin.cn/post/7103192601515425823

改写后,相关实现类只保留一个Activity和一个自定义View,方便阅读。直接贴源码和效果:

1.主Activity

package com.example.myapplication;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import androidx.annotation.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class StrokeOrderActivityNew extends Activity {
    String svgSix = null;
    String svgOne = null;
    StrokeOrderView strokeOrderView1;
    StrokeOrderView strokeOrderView2;
    Button btnSix;
    Button btnOne;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_stroke_order_layout);

        strokeOrderView1 = (StrokeOrderView)findViewById(R.id.stroke_order_view1);
        strokeOrderView2 = (StrokeOrderView)findViewById(R.id.stroke_order_view2);
        btnSix = (Button)findViewById(R.id.btn_load_svg_six);
        btnSix.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String name = "六.json"; // 需要将 svg.json 放在 assets 或特定路径下
                svgSix = loadSvgJson(name);
                strokeOrderView1.setStrokesBySvg(svgSix);
            }
        });

        btnOne = (Button)findViewById(R.id.btn_load_svg_one);
        btnOne.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String name = "一.json";
                svgOne = loadSvgJson(name);
                strokeOrderView2.setStrokesBySvg(svgOne);
            }
        });
    }

    private String loadSvgJson(String file) {
        BufferedReader reader = null;
        InputStreamReader inputStreamReader = null;
        try {
            InputStream inputStream = getAssets().open(file);
            inputStreamReader = new InputStreamReader(inputStream);
            reader = new BufferedReader(inputStreamReader);
            String line;
            StringBuilder entity = new StringBuilder();
            while ((line = reader.readLine()) != null) {
                entity.append(line);
            }
            return entity.toString();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                inputStreamReader.close();
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

2.自定义View,负责实现动画绘制效果

package com.example.myapplication;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.*;
import android.graphics.PathMeasure;
import android.graphics.Point;
import android.graphics.PorterDuff;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.core.graphics.PathParser;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;

public class StrokeOrderView extends View {

    static float SVG_STROKE_WIDTH = 1024F;
    static float SVG_STROKE_HEIGHT = 1024F;

    private ArrayList<Path> strokePaths = new ArrayList<Path>();
    private ArrayList<Path> medians = new ArrayList<Path>();
    private Paint strokePaint = new Paint();
    private Paint medianPaint = new Paint();
    private ArrayList<PathMeasure> medianMeasures = new ArrayList<PathMeasure>();
    private Path tempPath = new Path();
    private float progress = 0F;
    private int currIndex = 0;
    private ArrayList<Point> points = new ArrayList<Point>();

    Bitmap srcBmp = null;
    Canvas srcCanvas = null;
    Paint srcPaint = new Paint();

    Bitmap dstBmp = null;
    Canvas dstCanvas = null;
    Paint dstPaint = new Paint();

    PorterDuffXfermode clearMode = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
    PorterDuffXfermode srcMode = new PorterDuffXfermode(PorterDuff.Mode.SRC);
    PorterDuffXfermode porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);

    public StrokeOrderView(Context context) {
        super(context);
    }

    public StrokeOrderView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        strokePaint.setAntiAlias(true);
        strokePaint.setStyle(Paint.Style.FILL);
        strokePaint.setColor(Color.RED);

        medianPaint.setAntiAlias(true);
        medianPaint.setStyle(Paint.Style.FILL);
        medianPaint.setColor(Color.BLACK);

        srcPaint.setAntiAlias(true);
        srcPaint.setStrokeWidth(100f);
        srcPaint.setStyle(Paint.Style.STROKE);
        srcPaint.setColor(Color.BLACK);

        dstPaint.setAntiAlias(true);
        dstPaint.setStyle(Paint.Style.FILL);
        dstPaint.setColor(Color.BLACK);
    }

    public StrokeOrderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public StrokeOrderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    void setStrokesBySvg(String svgJson){
        strokePaths.clear();
        medians.clear();
        medianMeasures.clear();
        points.clear();
        ArrayList<String> strokes = new ArrayList<String>();
        parseSvgJson(svgJson, strokes, medians, points);
        for(int i=0; i<strokes.size(); i++){
            strokePaths.add(PathParser.createPathFromPathData(strokes.get(i)));
        }
        for(int i=0; i<medians.size(); i++){
            medianMeasures.add(new PathMeasure(medians.get(i), false));
        }
        startAnimation();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (0 == currIndex) {
            dstPaint.setXfermode(clearMode);
            if(dstCanvas != null){
                dstCanvas.drawPaint(dstPaint);
            }
            srcPaint.setXfermode(clearMode);
            if(srcCanvas != null){
                srcCanvas.drawPaint(srcPaint);
            }
        }
        float xTmp = getMeasuredWidth() / SVG_STROKE_WIDTH;
        float yTmp = getMeasuredHeight() / SVG_STROKE_HEIGHT;

        int restore = canvas.save();
        // 1. 沿着 y 轴旋转
        // 2. 将 View 下移自身高度,注意汉字上下有边,一般为字体高度的 1/8
        // 3. 按控件大小等比缩放汉字,注意默认(0, 0)左下缩,现在要求左上缩
        canvas.scale(1F, -1F);
        canvas.translate(0F, -SVG_STROKE_HEIGHT * 7 / 8); // -1024 + 1024/8
        canvas.scale(xTmp, yTmp, 0F, SVG_STROKE_HEIGHT * 7 / 8); // 1024 - 1024/8
        for(int i=0; i<strokePaths.size(); i++){
            canvas.drawPath(strokePaths.get(i), strokePaint);
        }
        canvas.restoreToCount(restore);

        float w = getMeasuredWidth();
        float h = getMeasuredHeight();
        int layer = canvas.saveLayer(0F, 0F, w, h, null);
        // 目标Bitmap

        if (dstBmp == null) {
            dstBmp = Bitmap.createBitmap((int)w, (int)h, Bitmap.Config.ARGB_8888);
            dstCanvas = new Canvas(dstBmp);
        }
        dstPaint.setXfermode(srcMode);// 只保留 srcBmp 的 alpha 和 color ,所以绘制出来只有源图

        int c1 = dstCanvas.save();
        dstCanvas.scale(1F, -1F);
        dstCanvas.translate(0F, -SVG_STROKE_HEIGHT * 7 / 8);
        dstCanvas.scale(xTmp, yTmp, 0F, SVG_STROKE_HEIGHT * 7 / 8);
        for(int i = strokePaths.size() - 1; i>=0; i--){
            if(i <= currIndex && currIndex < strokePaths.size()){
                dstCanvas.drawPath(strokePaths.get(i), dstPaint);
            }
        }

        dstCanvas.restoreToCount(c1);
        dstPaint.setXfermode(null);
        canvas.drawBitmap(dstBmp, 0F, 0F, medianPaint);

        // 在两者相交的地方绘制源图像
        medianPaint.setXfermode(porterDuffXfermode);
        // src bitmap
        if (srcBmp == null) {
            srcBmp = Bitmap.createBitmap((int)w, (int)h, Bitmap.Config.ARGB_8888);
            srcCanvas = new Canvas(srcBmp);
        }
        srcPaint.setXfermode(srcMode);// 只保留 srcBmp 的 alpha 和 color ,所以绘制出来只有源图

        int c2 = srcCanvas.save();
        srcCanvas.scale(1F, -1F);
        srcCanvas.translate(0F, -SVG_STROKE_HEIGHT * 7 / 8);
        srcCanvas.scale(xTmp, yTmp, 0F, SVG_STROKE_HEIGHT * 7 / 8);
        if (!medianMeasures.isEmpty()) { // 绘制当前进度下的笔画
            // 起笔落笔 都添加一个圆,防止不完整
            drawBackbonePointCircle(currIndex * 2, 20F);
            if (progress > 0.99) {
                drawBackbonePointCircle(currIndex * 2 + 1, 30F);
            }

            tempPath.reset();
            PathMeasure m = medianMeasures.get(currIndex);
            m.getSegment(0F, m.getLength() * progress, tempPath, true);
            srcCanvas.drawPath(tempPath, srcPaint);
        }
        srcCanvas.restoreToCount(c2);
        srcPaint.setXfermode(null);
        canvas.drawBitmap(srcBmp, 0F, 0F, medianPaint);
        medianPaint.setXfermode(null);

        canvas.restoreToCount(layer);
    }

    void parseSvgJson(String json, ArrayList<String> list, ArrayList<Path> paths, ArrayList<Point> points){
        try {
            JSONObject obj = new JSONObject(json);
            JSONArray array = obj.getJSONArray("strokes");
            for(int i=0; i<array.length(); i++){
                list.add(array.getString(i));
            }

            JSONArray array2 = obj.getJSONArray("medians");
            for(int i=0; i<array2.length(); i++){
                JSONArray array3 = array2.getJSONArray(i);
                Path path = new Path();
                for(int j=0; j<array3.length(); j++){
                    JSONArray array4 = array3.getJSONArray(j);
                    int x = array4.getInt(0);
                    int y = array4.getInt(1);
                    if(0 == j){
                        path.moveTo(x,y);
                        points.add(new Point(x,y));
                    }else{
                        path.lineTo(x,y);
                    }
                    if (array3.length() - 1 == j) { // 落笔骨干点
                        points.add(new Point(x, y));
                    }
                }
                paths.add(path);
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    private void drawBackbonePointCircle(int index, float radius) {
        srcPaint.setStyle(Paint.Style.FILL);
        // 其中points 为起笔落笔骨干点,size = 2 * medianPaint
        srcCanvas.drawCircle((float)points.get(index).x, (float)points.get(index).y, radius, srcPaint);
        srcPaint.setStyle(Paint.Style.STROKE);
    }

    private AnimatorSet createAnimation(){
        AnimatorSet set = new AnimatorSet();
        ArrayList<Animator> animators = new ArrayList<Animator>();
        for(int i=0; i<medians.size(); i++){
            ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
            final int index = i;
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator valueAnimator) {
                    currIndex = index;
                    progress = (float)valueAnimator.getAnimatedValue();
                    if(progress <= 1){
                        sleepAnimation();
                        postInvalidate();
                    }
                }
            });
            animator.setDuration(1000);
            animators.add(animator);
        }
        set.playSequentially(animators);
        return set;
    }

    private void sleepAnimation() {
        try {
            Thread.sleep(15);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void startAnimation(){
        AnimatorSet animator = createAnimation();
        animator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {
                progress = 0F;
                currIndex = 0;
            }

            @Override
            public void onAnimationEnd(Animator animator) {

            }

            @Override
            public void onAnimationCancel(Animator animator) {

            }

            @Override
            public void onAnimationRepeat(Animator animator) {
                progress = 0F;
                currIndex = 0;
            }
        });
        animator.start();
    }

}

3.补全布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="20dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">
            <Button
                android:id="@+id/btn_load_svg_six"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="六"
                android:textAllCaps="false"
                />

            <Button
                android:id="@+id/btn_load_svg_one"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="一"
                android:textAllCaps="false"
                />
        </LinearLayout>

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:gravity="center"
        android:orientation="horizontal">

        <com.example.myapplication.StrokeOrderView
            android:id="@+id/stroke_order_view1"
            android:layout_width="120dp"
            android:layout_height="120dp"
            android:background="#aaa"/>

        <com.example.myapplication.StrokeOrderView
            android:id="@+id/stroke_order_view2"
            android:layout_width="120dp"
            android:layout_height="120dp"
            android:layout_marginStart="24dp"
            android:background="#aaa"/>
    </LinearLayout>

</LinearLayout>

4.注意Assert下放置汉字的json文件,json文件不方便贴,大家可以从github上获取。

hanzi-writer-dataicon-default.png?t=M4ADhttps://github.com/chanind/hanzi-writer-data

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿部春光

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值