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

且看个效果图:

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

对于线条的绘制我们不多说,直接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)。

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

AnimatedPathView实现自定义图片标签的更多相关文章

  1. CKEditor5 + vue2.0 自定义图片上传、highlight、字体等用法

    因业务需求,要在 vue2.0 的项目里使用富文本编辑器,经过调研多个编辑器,CKEditor5 支持 vue,遂采用.因 CKEditor5 文档比较少,此处记录下引用和一些基本用法. CKEdit ...

  2. DedeCMS织梦自定义图片字段调用出现{dede:img ..}

    做站过程中碰到这样一个问题,找到解决办法收藏分享:为什么在首页用自定义列表调用出来的图片字段不是正确的图片地址,而是类似于: {dede:img text='' width='270' height= ...

  3. TensorFlow2.0(10):加载自定义图片数据集到Dataset

    .caret, .dropup > .btn > .caret { border-top-color: #000 !important; } .label { border: 1px so ...

  4. [19/06/04-星期二] HTML基础_实体(转义字符)、图片标签(img)、元标签(meta)、语法规范、内联框架(iframe)、超链接

    一.实体(转义字符) 在HTML中,一些诸如<.> 就是普通的小于号和大于号不能直接使用,因为浏览可能会把它当成一个标签去解析,所以需要一些特殊字符去表示这些特殊字符, 这些字符我们称他们 ...

  5. [19/05/14-星期二] HTML_body标签(列表标签和图片标签)

    一.列表标签 <!-- 快捷键 1.<meta charset="UTF-8"/> 用m6可直接写出 2.复制当前1行到下一行 ctrl+shift+R --&g ...

  6. DEDE建站之图片标签技巧指南

    做dede站的时候,曾经遇到很苦恼的事情,就是给图片集添加了一个网上下载下来的特效,需要给图片的链接上添加一个rel属性,供JS调用以达到那种特效.但是当时只知道dede的图片链接标签是[field: ...

  7. 使用自定义tld标签简化jsp的繁琐操作

    最近做一个树形结构的展示,请求目标页面后,后台只返回简单的List,虽然有想过在jsp页面内做一些操作简化,但是太繁琐了,其他的标签又不能满足需求,所以只能自己做一个.使用tld标签可以简化jsp代码 ...

  8. php正则获取html图片标签信息(采集图片)

    php获取html图片标签信息(采集图片),实现图片采集及其他功能,带代码如下: <?php $str="<img src='./a.jpg'/>111111<img ...

  9. 网页qq客服代码并自定义图片

    <script>var online= new Array();</script> <script src="http://webpresence.qq.com ...

随机推荐

  1. django 发送手机验证码

    一.流程分析: 1.用户在项目前端,输入手机号,然后点击[获取验证码],将手机号发到post到后台. 2.后台验证手机号是否合法,是否已被占用,如果通过验证,则生成验证码,并通过运行脚本,让短信运营商 ...

  2. Oracle 导入、导出DMP(备份)文件

    首先说明dmp文件: Oracle备份文件是以dmp结尾,这种文件是oracle的逻辑备份文件,常用于数据库逻辑备份,数据库迁移等操作. 一.Oracle导入备份文件步骤:我用的是Oracle 11g ...

  3. Java内存分配、管理小结

    转载自:http://java-mzd.iteye.com/blog/848635

  4. JavaScript数据结构和算法----栈

    前言 栈是一种遵循后进先出(LIFO)原则的有序集合,新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另外一端就叫栈底.在栈里,新元素都靠近栈顶,旧元素都接近栈底.可以想象桌上的一叠书,或者厨房里的 ...

  5. JMeter如何和Charles进行接口测试

    什么是接口测试,接口测试到底如何开展,我相信任何一个软件测试人员都会有这样的疑问, 这里我以接口测试自动化平台的登录接口进行讲解. 一.什么是接口测试? 接口测试是测试系统组件间接口的一种测试.接口测 ...

  6. JS基础(二)

    一.JS中的循环结构 循环结构的执行步骤 1.声明循环变量: 2.判断循环条件: 3.执行循环体操作: 4.更新循环变量 然后,循环执行2-4,直到条件不成立时,跳出循环. while循环()中的表达 ...

  7. 浅析JS异步执行机制

    前言 JS异步执行机制具有非常重要的地位,尤其体现在回调函数和事件等方面.本文将针对JS异步执行机制进行一个简单的分析. 从一份代码讲起 下面是两个经典的JS定时执行函数,这两个函数的区别相信对JS有 ...

  8. Vue国际化处理 vue-i18n 以及项目自动切换中英文

    1. 环境搭建 命令进入项目目录,执行以下命令安装vue 国际化插件vue-i18n npm install vue-i18n --save 2. 项目增加国际化翻译文件 在项目的src下添加lang ...

  9. UltraISO安装centos7系统

    1. 使用最新版UltraISO将ISO镜像刻录到U盘一定要是最新版,试用版都可以,按下图操作: 2. U盘启动电脑进入安装界面正常情况下你应该会看到下面的这个界面: 选择第一项,然后按TAB键(在评 ...

  10. Docker多台物理主机之间的容器互联

    Docker 默认的桥接网卡是 docker0.它只会在本机桥接所有的容器网卡,举例来说容器的虚拟网卡在主机上看一般叫做 veth* 而 Docker 只是把所有这些网卡桥接在一起,如下: [root ...