自定义View实现 “手机淘宝”物流进程模块进度告知UI横向版

转载请注明出处:王亟亟的大牛之路

话不多说,先洗脑,安利!!!https://github.com/ddwhan0123/Useful-Open-Source-Android 旅游都在更啊!!

这些天都在浪几乎没撸代码,然后今天下午找了个下午茶时间捯饬了个自定义View来实现 很多APP都有却没怎么公开的一个“进度通知的View”

实现power by:https://dribbble.com/LeslyPyram

先上下原设计:

用圆+线条+颜色的变化来告知用户你现在的物件到哪了(这是我自己买的东西的截图,看实现就好)

再贴下,今天实现的效果:

颜色不管,别的大致就差不多“翻版了”(间距和球体空心实心只是个人爱好问题,这不是模仿难点)

来看下项目目录

东西很简单,就一个控件就能搞定,先来看看如何使用

  <stepperindicator.com.stepperindicatordemo.MyStepperIndicator
        android:id="@+id/mySI"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/button"
        android:layout_marginLeft="32dp"
        android:layout_marginRight="32dp"
        android:layout_marginTop="32dp"
        app:stpi_stepCount="5" />

在布局文件里设置下总的“进程数”就可以了。

如果你要设置球的颜色,大小,边框粗细,如下:

 app:stpi_circleColor

 app:stpi_circleRadius

 app:stpi_circleStrokeWidth

当然,你也有其他的自定义选项,这里不做全部介绍了,你可以看 attrs.xml文件内的标签,命名很规范,一看就懂

看完怎么用,来看下怎么实现的

public class MyStepperIndicator extends View {
    private static final float EXPAND_MARK = 1.3f;
    private static final int DEFAULT_ANIMATION_DURATION = 250;
    private Resources resources;
    private Paint circlePaint;
    private Paint linePaint, lineDonePaint, lineDoneAnimatedPaint;
    private Paint indicatorPaint;
    private float circleRadius;
    private float animProgress;
    private float checkRadius;
    private float animIndicatorRadius;
    private float indicatorRadius;
    private float lineLength;
    private float lineMargin;
    private float animCheckRadius;
    private int animDuration;
    private int stepCount;
    private int currentStep;
    private Bitmap doneIcon;
    private float[] indicators;
    private List<Path> linePathList = new ArrayList<>();
    private AnimatorSet animatorSet;
    private ObjectAnimator lineAnimator, indicatorAnimator, checkAnimator;
    private int previousStep;

    //构造函数
    public MyStepperIndicator(Context context) {
        this(context, null);
    }

    public MyStepperIndicator(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyStepperIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public MyStepperIndicator(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context, attrs, defStyleAttr);
    }

    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        //获取项目资源用
        resources = getResources();
        // 默认值
        int defaultCircleColor = ContextCompat.getColor(context, R.color.stpi_default_circle_color);
        float defaultCircleRadius = resources.getDimension(R.dimen.stpi_default_circle_radius);
        float defaultCircleStrokeWidth = resources.getDimension(R.dimen.stpi_default_circle_stroke_width);

        int defaultIndicatorColor = ContextCompat.getColor(context, R.color.stpi_default_indicator_color);
        float defaultIndicatorRadius = resources.getDimension(R.dimen.stpi_default_indicator_radius);

        float defaultLineStrokeWidth = resources.getDimension(R.dimen.stpi_default_line_stroke_width);
        float defaultLineMargin = resources.getDimension(R.dimen.stpi_default_line_margin);
        int defaultLineColor = ContextCompat.getColor(context, R.color.stpi_default_line_color);
        int defaultLineDoneColor = ContextCompat.getColor(context, R.color.stpi_default_line_done_color);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StepperIndicator, defStyleAttr, 0);

        circlePaint = new Paint();
        circlePaint.setStrokeWidth(a.getDimension(R.styleable.StepperIndicator_stpi_circleStrokeWidth, defaultCircleStrokeWidth));
        circlePaint.setStyle(Paint.Style.STROKE);
        circlePaint.setColor(a.getColor(R.styleable.StepperIndicator_stpi_circleColor, defaultCircleColor));
        circlePaint.setAntiAlias(true);

        indicatorPaint = new Paint(circlePaint);
        indicatorPaint.setStyle(Paint.Style.FILL);
        indicatorPaint.setColor(a.getColor(R.styleable.StepperIndicator_stpi_indicatorColor, defaultIndicatorColor));
        indicatorPaint.setAntiAlias(true);

        linePaint = new Paint();
        linePaint.setStrokeWidth(a.getDimension(R.styleable.StepperIndicator_stpi_lineStrokeWidth, defaultLineStrokeWidth));
        linePaint.setStrokeCap(Paint.Cap.ROUND);
        linePaint.setStyle(Paint.Style.STROKE);
        linePaint.setColor(a.getColor(R.styleable.StepperIndicator_stpi_lineColor, defaultLineColor));
        linePaint.setAntiAlias(true);

        lineDonePaint = new Paint(linePaint);
        lineDonePaint.setColor(a.getColor(R.styleable.StepperIndicator_stpi_lineDoneColor, defaultLineDoneColor));

        lineDoneAnimatedPaint = new Paint(lineDonePaint);

        circleRadius = a.getDimension(R.styleable.StepperIndicator_stpi_circleRadius, defaultCircleRadius);
        checkRadius = circleRadius + circlePaint.getStrokeWidth() / 2f;
        indicatorRadius = a.getDimension(R.styleable.StepperIndicator_stpi_indicatorRadius, defaultIndicatorRadius);
        animIndicatorRadius = indicatorRadius;
        animCheckRadius = checkRadius;
        lineMargin = a.getDimension(R.styleable.StepperIndicator_stpi_lineMargin, defaultLineMargin);

        setStepCount(a.getInteger(R.styleable.StepperIndicator_stpi_stepCount, 2));
        animDuration = a.getInteger(R.styleable.StepperIndicator_stpi_animDuration, DEFAULT_ANIMATION_DURATION);

        a.recycle();

        doneIcon = BitmapFactory.decodeResource(resources, R.drawable.ic_done_white_18dp);

        if (isInEditMode())
            currentStep = Math.max((int) Math.ceil(stepCount / 2f), 1);
    }

    public void setStepCount(int stepCount) {
        if (stepCount < 2)
            throw new IllegalArgumentException("stepCount must be >= 2");
        this.stepCount = stepCount;
        currentStep = 0;
        compute();
        invalidate();
    }

    private void compute() {
        indicators = new float[stepCount];
        linePathList.clear();

        float startX = circleRadius * EXPAND_MARK + circlePaint.getStrokeWidth() / 2f;

        // 计算指标和线路长度的位置
        float divider = (getMeasuredWidth() - startX * 2f) / (stepCount - 1);
        lineLength = divider - (circleRadius * 2f + circlePaint.getStrokeWidth()) - (lineMargin * 2);

        // 计算圈和线的位置
        for (int i = 0; i < indicators.length; i++)
            indicators[i] = startX + divider * i;
        for (int i = 0; i < indicators.length - 1; i++) {
            float position = ((indicators[i] + indicators[i + 1]) / 2) - lineLength / 2;
            final Path linePath = new Path();
            linePath.moveTo(position, getMeasuredHeight() / 2);
            linePath.lineTo(position + lineLength, getMeasuredHeight() / 2);
            linePathList.add(linePath);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        Log.d("--->onSizeChanged", "w = " + w + " h = " + h + " oldw = " + oldw + " oldh = " + oldh);
        compute();
    }

    //控件绘图
    @SuppressWarnings("ConstantConditions")
    @Override
    protected void onDraw(Canvas canvas) {
        float centerY = getMeasuredHeight() / 2f;

        // 目前绘制动画步骤n-1 n,或从n + 1到n
        boolean inAnimation = false;
        boolean inLineAnimation = false;
        boolean inIndicatorAnimation = false;
        boolean inCheckAnimation = false;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            inAnimation = animatorSet != null && animatorSet.isRunning();
            inLineAnimation = lineAnimator != null && lineAnimator.isRunning();
            inIndicatorAnimation = indicatorAnimator != null && indicatorAnimator.isRunning();
            inCheckAnimation = checkAnimator != null && checkAnimator.isRunning();
        }

        boolean drawToNext = previousStep == currentStep - 1;
        boolean drawFromNext = previousStep == currentStep + 1;

        for (int i = 0; i < indicators.length; i++) {
            final float indicator = indicators[i];
            boolean drawCheck = i < currentStep || (drawFromNext && i == currentStep);

            // 画圈
            canvas.drawCircle(indicator, centerY, circleRadius, circlePaint);

            // 如果当前步骤,或回来,下一步还有返回动画
            if ((i == currentStep && !drawFromNext) || (i == previousStep && drawFromNext && inAnimation)) {
                // Draw animated indicator
                canvas.drawCircle(indicator, centerY, animIndicatorRadius, indicatorPaint);
            }

            // 画对勾
            if (drawCheck) {
                float radius = checkRadius;
                if ((i == previousStep && drawToNext)
                        || (i == currentStep && drawFromNext))
                    radius = animCheckRadius;
                canvas.drawCircle(indicator, centerY, radius, indicatorPaint);
                if (!isInEditMode()) {
                    if ((i != previousStep && i != currentStep) || (!inCheckAnimation && !(i == currentStep && !inAnimation)))
                        canvas.drawBitmap(doneIcon, indicator - (doneIcon.getWidth() / 2), centerY - (doneIcon.getHeight() / 2), null);
                }
            }

            // 画线
            if (i < linePathList.size()) {
                if (i >= currentStep) {
                    canvas.drawPath(linePathList.get(i), linePaint);
                    if (i == currentStep && drawFromNext && (inLineAnimation || inIndicatorAnimation)) // Coming back from n+1
                        canvas.drawPath(linePathList.get(i), lineDoneAnimatedPaint);
                } else {
                    if (i == currentStep - 1 && drawToNext && inLineAnimation) {
                        // Going to n+1
                        canvas.drawPath(linePathList.get(i), linePaint);
                        canvas.drawPath(linePathList.get(i), lineDoneAnimatedPaint);
                    } else
                        canvas.drawPath(linePathList.get(i), lineDonePaint);
                }
            }
        }
    }

    //控件尺寸
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int desiredHeight = (int) Math.ceil((circleRadius * EXPAND_MARK * 2) + circlePaint.getStrokeWidth());

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width = widthMode == MeasureSpec.EXACTLY ? widthSize : getSuggestedMinimumWidth();
        int height = heightMode == MeasureSpec.EXACTLY ? heightSize : desiredHeight;

        setMeasuredDimension(width, height);
    }

    public int getStepCount() {
        return stepCount;
    }

    public int getCurrentStep() {
        return currentStep;
    }

    /**
     * 设置现在的位置
     *
     * @param currentStep a value between 0 (inclusive) and stepCount (inclusive)
     */
    @UiThread
    public void setCurrentStep(int currentStep) {
        if (currentStep < 0 || currentStep > stepCount)
            throw new IllegalArgumentException("Invalid step value " + currentStep);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            if (animatorSet != null)
                animatorSet.cancel();
            animatorSet = null;
            lineAnimator = null;
            indicatorAnimator = null;

            if (currentStep == this.currentStep + 1) {
                previousStep = this.currentStep;
                animatorSet = new AnimatorSet();

                // 画新的线
                lineAnimator = ObjectAnimator.ofFloat(MyStepperIndicator.this, "animProgress", 1.0f, 0.0f);

                // 标记
                checkAnimator = ObjectAnimator
                        .ofFloat(MyStepperIndicator.this, "animCheckRadius", indicatorRadius, checkRadius * EXPAND_MARK, checkRadius);

                // 最后到达目标"进度"
                animIndicatorRadius = 0;
                indicatorAnimator = ObjectAnimator
                        .ofFloat(MyStepperIndicator.this, "animIndicatorRadius", 0f, indicatorRadius * 1.4f, indicatorRadius);

                animatorSet.play(lineAnimator).with(checkAnimator).before(indicatorAnimator);
            } else if (currentStep == this.currentStep - 1) {
                //回退操作
                previousStep = this.currentStep;
                animatorSet = new AnimatorSet();

                indicatorAnimator = ObjectAnimator.ofFloat(MyStepperIndicator.this, "animIndicatorRadius", indicatorRadius, 0f);

                animProgress = 1.0f;
                lineDoneAnimatedPaint.setPathEffect(null);
                lineAnimator = ObjectAnimator.ofFloat(MyStepperIndicator.this, "animProgress", 0.0f, 1.0f);

                animCheckRadius = checkRadius;
                checkAnimator = ObjectAnimator.ofFloat(MyStepperIndicator.this, "animCheckRadius", checkRadius, indicatorRadius);

                animatorSet.playSequentially(indicatorAnimator, lineAnimator, checkAnimator);
            }

            if (animatorSet != null) {
                lineAnimator.setDuration(Math.min(500, animDuration));
                lineAnimator.setInterpolator(new DecelerateInterpolator());
                indicatorAnimator.setDuration(lineAnimator.getDuration() / 2);
                checkAnimator.setDuration(lineAnimator.getDuration() / 2);

                animatorSet.start();
            }
        }

        this.currentStep = currentStep;
        invalidate();
    }

    public void setAnimIndicatorRadius(float animIndicatorRadius) {
        this.animIndicatorRadius = animIndicatorRadius;
        invalidate();
    }

    public void setAnimCheckRadius(float animCheckRadius) {
        this.animCheckRadius = animCheckRadius;
        invalidate();
    }

}

重要步骤已经写在里面了,这里做下详细解释

1.初始化一系列需要的参数(从xml获得或者通过set方法)

2.位置计算

3.在draw方法里根据获取的参数进行一系列的绘制,绘制内容为 点,圆 线。

然后就是一系列逻辑和动画了(实现概念很基础,但是基础很重要)

setCurrentStep方法为最为关键的动画实现方法。根据当前“进度”值currentStep作为逻辑判断的标准。

源码地址:https://github.com/ddwhan0123/BlogSample/blob/master/StepperIndicatorDemo/StepperIndicatorDemo.zip?raw=true

有问题可以微信我(活人)

谢谢阅读

时间: 2024-08-25 14:26:37

自定义View实现 “手机淘宝”物流进程模块进度告知UI横向版的相关文章

自定义View之仿淘宝详情页

自定义View之仿淘宝详情页 转载请标明出处: http://blog.csdn.net/lisdye2/article/details/52353071 本文出自:[Alex_MaHao的博客] 项目中的源码已经共享到github,有需要者请移步[Alex_MaHao的github] 基本介绍 现在的一些购物类App例如淘宝,京东等,在物品详情页,都采用了类似分层的模式,即上拉加载详情的方式,节省了空间,使用户的体验更加的舒适.只要对于某个东西的介绍很多时,都可以采取这样的方式,第一个页面显示

手机淘宝 521 性能优化项目揭秘

又是一年双十一,亿万用户都会在这一天打开手机淘宝,高兴地在会场页面不断浏览,面对琳琅满目的商品图片,抢着添加购物车,下单付款.为了让用户更顺畅更方便地实现这一切,做到“如丝般顺滑”,双十一前夕手机淘宝成立了“521”(我爱你)性能优化项目,在日常优化基础之上进行三个方面的专项优化攻关,分别是1)H5页面的一秒法则:2)启动时间和页面帧率提升20%:3)Android内存占用降低50%.优化过程中遇到的困难,思考后找寻的方案,实施后提取的经验都会在下面详细地介绍给读者. 第一章 一秒法则的实现 “

电商客户端竞品分析-手机淘宝、京东、唯品会、聚美优品

市场状况 艾瑞最新统计数据显示,2013年移动网购整体交易规模1676.4亿元,同比增幅高达165.4%,而同期PC端网购规模将近16000多亿元,同比增速35.7%.预计2017年市场规模将近万亿,增速39.4%. 图1 2011-2017年中国移动购物市场交易规模 2013年中国移动网购渗透率9.1%,同比去年增长超过四个百分点.预计到2017年移动购物渗透率达到24.1%. 图2 2011-2017年中国网购交易额PC端和移动端占比 2013年移动网购企业份额中,手机淘宝占比76.1%,京

高效、稳定、可复用——手机淘宝主会场框架详解

导读: 为了让消费者在大促时能更快挑选到商品和进行平台的营销互动,手机淘宝都会有一个页面来承载大促的核心内容和主要的营销要素,称之为主会场.本文重点分享 了从技术和业务上如何提升主会场效能,将浏览体验做到最优,让用户能够更快地找到自己感兴趣的内容,提高转化率,应对任何业务变化,保障大促的顺利进行. 每次为了让消费者在大促时能更快挑选到商品和进行平台的营销互动,都会有一个页面来承载大促的核心内容和主要的营销要 素,我们称之为主会场.以前主会场开发,都是前端通过页面搭建系统或者以源码方式制作PC页面

《转》冯森林:手机淘宝中的那些Web技术(2014年)

Native APP与Web APP的技术融合已经逐渐成为一种趋势,使用标准的Web技术来开发应用中的某些功能,不仅可以降低开发成本,同时还可以方便的进行功能迭代更新.但是如何保证Web APP的流畅性也一直是业内讨论的热点.InfoQ此次专访了手机淘宝客户端高级技术专家冯森林来谈谈手机淘宝在Web技术方面的一些实践经验,另外作为ArchSummit深圳2014大会<移动互联网,一浪高过一浪>专题的讲师,冯森林将会分享 手机淘宝的客户端架构探索之路 . InfoQ:淘宝手机客户端是否使用了HT

手机淘宝Android客户端架构

手机淘宝Android客户端有几百人开发,十几个团队.如果整个Android客户端是一个工程,那十几个团队每个人上午上班第一件事情估计就是合代码,运气不好,一天都在合代码,而且只要有一个人提交的代码编译不过,所有人都会被堵塞在那里,所以单个工程是不可能的事情. 只要是包含了很多业务的客户端,都会面临这个问题,各个业务代码量越来越多,新需求又源源不断的来,业务团队之间要是有直接依赖,那被依赖最多的团队成员,在改代码的时候都是战战兢兢的,生怕自己的改动导致其他业务奔溃.最终交付的时候,总会被一个业务

手机淘宝构架演化实践

2014年12月19日~20日,ArchSummit北京2014大会顺利举行.“移动互联网,随时随地”是非常火爆的一个专题.阿里无线事业部技术负责人庄卓然(花名南天)任出品人.来自阿里无线事业部的高级专家李敏(花名心石,微博:@allblue_华丽地低调 )分享了<手机淘宝架构演化实践>(幻灯片下载). 李敏主要负责淘宝无线客户端和无线网站基础服务.购物主链路的架构.研发方面的工作.从09年开始参与手机淘宝研发团队的组建和线上产品研发,先后负责过无线部门的社区.会员.营销.交易等多条产品线的技

Android热补丁技术—dexposed原理简析(手机淘宝采用方案)

本文由嵌入式企鹅圈原创团队成员.阿里资深工程师Hao分享. 上篇文章<Android无线开发的几种常用技术>我们介绍了几种android移动应用开发中的常用技术,其中的热补丁正在被越来越多的开发团队所使用,它涉及到dalvik虚拟机和android的一些核心技术,现在就来介绍下它的一些原理. 本篇先介绍dexposed方案:https://github.com/alibaba/dexposed,它是手机淘宝团队使用的热补丁方案,后来开源到github上,取的名字dexposed表明了自己是基于

手机淘宝推荐中的排序学习

原文:http://yq.aliyun.com/articles/122?spm=0.0.0.0.oL8bTY 周梁:淘宝推荐机器学习技术专家,中国科学院自动化研究所机器学习博士,主要研究工作方向是机器学习.大规模并行算法优化.先后从事过广告CTR预估,MPI机器学习平台搭建,手淘个性化推荐等多方面工作. 排序学习是推荐.搜索.广告的核心问题.在手机淘宝的推荐场景中,受制于展示空间的限制,排序学习显得尤为重要.在淘宝,如何从十亿的商品中,挑选出用户 今天喜欢的商品,也是个巨大的挑战. 本次我们分