AnimatedPathView实现自定义图片标签详解手机开发

老早用过小红书app,对于他们客户端笔记这块的设计非常喜欢,恰好去年在小红书的竞争对手公司,公司基于产品的考虑和产品的发展,也需要将app社交化,于是在社区分享这块多多少少参照了小红书的设计,这里面就有一个比较有意思的贴纸,标签等设计,这里用到了GpuImage的库,这个demo我也将代码开源了,有需要的去fork我的github的代码,今天要说的是详情页面的AnimatedPathView实现可以动起来的标签。(之前我们项目中由于时间问题,将这种效果用h5实现了,不过现在回React Native之后,发现实现起来更简单了),今天要说的是用android实现这种效果。

且看个效果图:

AnimatedPathView实现自定义图片标签详解手机开发AnimatedPathView实现自定义图片标签详解手机开发AnimatedPathView实现自定义图片标签详解手机开发

要实现我们这样的效果,首先分析下,线条的绘制和中间圆圈的实现,以及文字的绘制。

对于线条的绘制我们不多说,直接canvas.DrawLine,不过这种线条是死的,不能实现运动的效果,还好Java为我们提供了另一个方法,我们可以用Path去实现,之前做腾讯手写板的时候也是这么做的(可以点击链接查看效果,不过代码没办法公开),点击打开链接,通过上面说的,我们改变PathEffect的偏移量就可以改变path显示的长度,从而实现动画的效果。而PathEffect有很多子类,从而满足不同的效果,这里不再说明。

	 
float percentage = 0.0f; 
PathEffect effect = new DashPathEffect(new float[]{pathLength, pathLength}, pathLength - pathLength*percentage); 

这里贴出AnimatedPathView的完整代码:

public class AnimatedPathView extends View { 
 
    private Paint mPaint; 
    private Path mPath; 
    private int mStrokeColor = Color.parseColor("#ff6c6c"); 
    private int mStrokeWidth = 8; 
 
    private float mProgress = 0f; 
    private float mPathLength = 0f; 
 
    private float circleX = 0f; 
    private float circleY = 0f; 
    private int radius = 0; 
    private String pathText="化妆包..."; 
    private int textX,textY; 
 
    public AnimatedPathView(Context context) { 
        this(context, null); 
        init(); 
    } 
 
    public AnimatedPathView(Context context, AttributeSet attrs) { 
        this(context, attrs, 0); 
        init(); 
    } 
 
    public AnimatedPathView(Context context, AttributeSet attrs, int defStyle) { 
        super(context, attrs, defStyle); 
 
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AnimatedPathView); 
        mStrokeColor = a.getColor(R.styleable.AnimatedPathView_pathColor, Color.parseColor("#ff6c6c")); 
        mStrokeWidth = a.getInteger(R.styleable.AnimatedPathView_pathWidth, 8); 
        a.recycle(); 
 
        init(); 
    } 
 
    private void init() { 
        mPaint = new Paint(); 
        mPaint.setColor(mStrokeColor); 
        mPaint.setStyle(Paint.Style.STROKE); 
        mPaint.setStrokeWidth(mStrokeWidth); 
        mPaint.setAntiAlias(true); 
 
        setPath(new Path()); 
    } 
 
    public void setPath(Path p) { 
        mPath = p; 
        PathMeasure measure = new PathMeasure(mPath, false); 
        mPathLength = measure.getLength(); 
    } 
 
 
    public void setPathText(String pathText,int textX,int textY ) { 
        this.pathText=pathText; 
        this.textX=textX; 
        this.textY=textY; 
    } 
 
    public void setPath(float[]... points) { 
        if (points.length == 0) 
            throw new IllegalArgumentException("Cannot have zero points in the line"); 
 
        Path p = new Path(); 
        p.moveTo(points[0][0], points[0][1]); 
 
        for (int i = 1; i < points.length; i++) { 
            p.lineTo(points[i][0], points[i][1]); 
        } 
        //将第一个xy坐标点作为绘制的原点 
        circleX = points[0][0] - radius / 2; 
        circleY = points[0][1] - radius / 2; 
 
        setPath(p); 
    } 
 
    public void setPercentage(float percentage) { 
        if (percentage < 0.0f || percentage > 1.0f) 
            throw new IllegalArgumentException("setPercentage not between 0.0f and 1.0f"); 
 
        mProgress = percentage; 
        invalidate(); 
    } 
 
    public void scalePathBy(float x, float y) { 
        Matrix m = new Matrix(); 
        m.postScale(x, y); 
        mPath.transform(m); 
        PathMeasure measure = new PathMeasure(mPath, false); 
        mPathLength = measure.getLength(); 
    } 
 
    public void scaleCircleRadius(int radius) { 
        this.radius = radius; 
    } 
 
    @Override 
    protected void onDraw(Canvas canvas) { 
        super.onDraw(canvas); 
        //绘制圆形 
//        drawCircle(canvas); 
        //绘线条 
        drawPathEffect(canvas); 
        //绘制文字 
        drawText(canvas); 
        canvas.restore(); 
    } 
 
    private void drawText(Canvas canvas) { 
        mPaint.setTextSize(28); 
        mPaint.setColor(Color.parseColor("#ffffff")); 
        if (canvas!=null&& !TextUtils.isEmpty(pathText)){ 
            canvas.drawText(pathText,textX,textY,mPaint); 
        } 
        invalidate(); 
    } 
 
    private void drawPathEffect(Canvas canvas) { 
        PathEffect pathEffect = new DashPathEffect(new float[]{mPathLength, mPathLength}, (mPathLength - mPathLength * mProgress)); 
        mPaint.setPathEffect(pathEffect); 
        mPaint.setStrokeWidth(4); 
        mPaint.setColor(Color.parseColor("#ffffff")); 
        canvas.save(); 
        canvas.translate(getPaddingLeft(), getPaddingTop()); 
        canvas.drawPath(mPath, mPaint); 
    } 
 
    private void drawCircle(Canvas canvas) { 
        int strokenWidth = 25; 
 
        mPaint.setStrokeWidth(strokenWidth); 
        mPaint.setColor(Color.parseColor("#ffffff")); 
        canvas.drawCircle(circleX, circleY, radius , mPaint); 
    } 
 
    @Override 
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
        super.onMeasure(widthMeasureSpec, heightMeasureSpec); 
        int widthSize = MeasureSpec.getSize(widthMeasureSpec); 
        int heightSize = MeasureSpec.getSize(heightMeasureSpec); 
        int widthMode = MeasureSpec.getMode(widthMeasureSpec); 
        int heightMode = MeasureSpec.getMode(widthMeasureSpec); 
 
        int measuredWidth, measuredHeight; 
 
        if (widthMode == MeasureSpec.AT_MOST) 
            throw new IllegalStateException("AnimatedPathView cannot have a WRAP_CONTENT property"); 
        else 
            measuredWidth = widthSize; 
 
        if (heightMode == MeasureSpec.AT_MOST) 
            throw new IllegalStateException("AnimatedPathView cannot have a WRAP_CONTENT property"); 
        else 
            measuredHeight = heightSize; 
 
        setMeasuredDimension(measuredWidth, measuredHeight); 
    } 
}

这段代码借鉴了
点击打开链接的部分代码,并在此基础上做了更多的判断和改变,以满足本文开头说说的那种需要,上面的代码只是实现了画线条的效果,那么如何实现中间圆圈的闪烁呢,其实也很简单,我们可以用动画来实现(View动画),这里我们大可以自己自定义一个View实现,而这个View包含了圆圈闪烁和画线,按照上面的逻辑我们写一个自定义的View,代码如下:

public class PointView extends FrameLayout { 
 
    private Context mContext; 
    private List<PointScaleBean> points; 
    private FrameLayout layouPoints; 
    private AnimatedPathView animatedPath; 
    private int radius=10; 
    private String text="图文标签 $99.00"; 
 
    public PointView(Context context) { 
        this(context, null); 
    } 
 
    public PointView(Context context, AttributeSet attrs) { 
        this(context, attrs, 0); 
    } 
 
    public PointView(Context context, AttributeSet attrs, int defStyleAttr) { 
        super(context, attrs, defStyleAttr); 
        initView(context, attrs); 
    } 
 
 
    private void initView(Context context, AttributeSet attrs) { 
        this.mContext = context; 
        View imgPointLayout = inflate(context, R.layout.layout_point, this); 
        layouPoints = (FrameLayout) imgPointLayout.findViewById(R.id.layouPoints); 
        animatedPath=(AnimatedPathView) imgPointLayout.findViewById(R.id.animated_path); 
    } 
 
 
    public void addPoints(int width, int height) { 
        addPoint(width, height); 
    } 
 
    public void setPoints(List<PointScaleBean> points) { 
        this.points = points; 
    } 
 
    private void addPoint(int width, int height) { 
        layouPoints.removeAllViews(); 
        for (int i = 0; i < points.size(); i++) { 
            double width_scale = points.get(i).widthScale; 
            double height_scale = points.get(i).heightScale; 
            LinearLayout view = (LinearLayout) LayoutInflater.from(mContext).inflate(R.layout.layout_img_point, this, false); 
            ImageView imageView = (ImageView) view.findViewById(R.id.imgPoint); 
            imageView.setTag(i); 
 
            AnimationDrawable animationDrawable = (AnimationDrawable) imageView.getDrawable(); 
            animationDrawable.start(); 
 
            LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); 
 
            layoutParams.leftMargin = (int) (width * width_scale); 
            layoutParams.topMargin = (int) (height * height_scale); 
 
//            imageView.setOnClickListener(this); 
 
            layouPoints.addView(view, layoutParams); 
        } 
        initView(); 
        initPathAnimated(); 
    } 
 
    private void initPathAnimated() { 
        ViewTreeObserver observer = animatedPath.getViewTreeObserver(); 
        if(observer != null){ 
            observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 
                @Override 
                public void onGlobalLayout() { 
                    animatedPath.getViewTreeObserver().removeGlobalOnLayoutListener(this); 
                    animatedPath.scaleCircleRadius(radius); 
                    animatedPath.scalePathBy(animatedPath.getWidth()/2,animatedPath.getHeight()/2); 
                    float[][] points = new float[][]{ 
                            {animatedPath.getWidth()/2-radius/2,animatedPath.getHeight()/2-radius/2}, 
                            {animatedPath.getWidth()/2- UIUtils.dp2px(mContext,30), animatedPath.getHeight()/2- UIUtils.dp2px(mContext,40)}, 
                            {animatedPath.getWidth()/2-UIUtils.dp2px(mContext,150), animatedPath.getHeight()/2- UIUtils.dp2px(mContext,40)}, 
                    }; 
                    animatedPath.setPath(points); 
//                    animatedPath.setPathText(text,animatedPath.getWidth()/2-UIUtils.dp2px(mContext,150), animatedPath.getHeight()/2- UIUtils.dp2px(mContext,50)); 
                } 
            }); 
        } 
    } 
 
    private void initView() { 
        animatedPath.setOnClickListener(new View.OnClickListener() { 
            @Override 
            public void onClick(View view) { 
                ObjectAnimator anim = ObjectAnimator.ofFloat(view, "percentage", 0.0f, 1.0f); 
                anim.setDuration(2000); 
                anim.setInterpolator(new LinearInterpolator()); 
                anim.start(); 
            } 
        }); 
    } 
 
}

上面对应的布局和资源文件:

layou_point.xml

<?xml version="1.0" encoding="utf-8"?> 
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="match_parent" 
    android:layout_height="wrap_content" 
    > 
 
    <com.yju.app.widght.path.AnimatedPathView 
        android:id="@+id/animated_path" 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" 
        /> 
 
    <FrameLayout 
        android:id="@+id/layouPoints" 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" 
        android:layout_gravity="center" /> 
 
</FrameLayout>

layout_img_point.xml

<?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:gravity="center" 
    android:orientation="vertical"> 
 
    <ImageView 
        android:id="@+id/imgPoint" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:src="@drawable/point_img" /> 
 
</LinearLayout>

文中用到的Anim就是帧动画了,

<?xml version="1.0" encoding="utf-8"?> 
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" 
    android:oneshot="false"> 
    <item 
        android:drawable="@drawable/point_img1" 
        android:duration="100" /> 
    ....省略n多图片资源 
    <item 
        android:drawable="@drawable/point_img13" 
        android:duration="100" /> 
</animation-list>

而最后我们只需要在我们自己的MainActivity中添加简单的代码既可实现上面的效果:

private void initPointView() { 
        List<PointScaleBean> list=new ArrayList<>(); 
        PointScaleBean point=new PointScaleBean(); 
        point.widthScale = 0.36f; 
        point.heightScale = 0.75f; 
        list.add(point); 
        pointView.setPoints(list); 
        pointView.addPoints(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT); 
    }

对于布局我是这么做的,将View的父布局的背景加一个图片,实际的开发中大家可以写一个相对的布局,这个就能实现实时的效果了,好了就写到这里,有疑问请留言或者加群(278792776)。

附件:一个滤镜效果:点击打开链接

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/tech/app/5651.html

(0)
上一篇 2021年7月17日 00:01
下一篇 2021年7月17日 00:01

相关推荐

发表回复

登录后才能评论