AnimatedPathView实现自定义图片标签,让图片动起来

    xiaoxiao2025-11-18  25

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

    且看个效果图:

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

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

    [html]  view plain  copy  print ?       float percentage = 0.0f;  PathEffect effect = new DashPathEffect(new float[]{pathLength, pathLength}, pathLength - pathLength*percentage);   这里贴出AnimatedPathView的完整代码:

    [html]  view plain  copy  print ? 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,代码如下:

    [html]  view plain  copy  print ? 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

    [html]  view plain  copy  print ? <?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

    [html]  view plain  copy  print ? <?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就是帧动画了,

    [html]  view plain  copy  print ? <?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中添加简单的代码既可实现上面的效果:

    [html]  view plain  copy  print ? 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)。 相关资源:AnimatedPathView.zip
    最新回复(0)