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

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

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

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

先上下原设计:

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

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

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

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

来看下项目目录


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

  <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

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

谢谢阅读

你可能感兴趣的:(UI,android,git)