您的位置:首页 > 其它

AnimatedPathView实现自定义图片标签

2016-11-20 13:35 381 查看
版权声明:本文为博主原创文章,未经博主允许不得转载。

老早用过小红书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);  



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);  
    }  
}  



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();  
            }  
        });  
    }  
  
}  



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>  



<?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>  



<?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>  



<?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);  
    }  



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)。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: