自定义View系列(1)--仿支付宝中物流状态效果

国际惯例,先上支付宝中的原效果图:                                                                                 

   


   再来一张自定义view的效果图



看到两个效果图的对比,可能会有人问为啥物流状态被选中时的背景没有?其实是有的,只不过我把代码注释掉了,原因就是背景太难看了,毕竟水平有限,画上去的背景和原效果差距甚远,不好意思展示出来,所以就把字体稍微放大一些作为突出.还请见谅~~

先说一下这个自定义view支持的属性哈

1,支持在代码里设置出发地和目的地(如效果图中的武汉市和兰州市)

2,支持在代码里和布局文件里设置最新状态时圆圈的颜色和线的颜色

3,支持最新状态所在位置,比如从服务端返回的json中拿到当前的状态为"派件中",那么直接调用setNewestPointPosition(2)即可,

都是最基本且必须的属性,当然还可以抽取出来其他属性,比如未选中的圆圈和线的颜色以及字体大小和颜色,圆圈半径,线的粗细,圆圈与线之间的padding值等.

下面上代码:

package com.lanma.customviewproject.views;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;

import com.lanma.customviewproject.R;

/**
 * 作者 qiang_xi on 2016/8/17 10:17.
 * 状态线:仿新版支付宝物流状态效果
 */
public class StateLine extends View {
    private Paint mCirclePaint;//圆圈画笔
    private Paint mLinePaint;//线画笔
    private Paint mTextPaint;//文字画笔
    private String[] data = {"已发货", "运输中", "派件中", "已签收"};
    private int pointPosition;//最新的点的位置
    private String startPositionText;//出发地
    private String arrivePositionText;//目的地
    private int selectedCircleColor = Color.RED;
    private int selectedLineColor = Color.RED;
    private Drawable mTextBackground = getContext().getResources().getDrawable(R.drawable.bg_pop_dialog);//选中的文字背景

    public StateLine(Context context) {
        this(context, null);
    }

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

    public StateLine(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
        TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.StateLine);

        for (int i = 0; i < a.getIndexCount(); i++) {
           int attr =  a.getIndex(i);
            switch (attr) {
                case R.styleable.StateLine_selectedCircleColor:
                    selectedCircleColor = a.getColor(attr, Color.RED);
                    break;
                case R.styleable.StateLine_selectedLineColor:
                    selectedLineColor = a.getColor(attr, Color.RED);
                    break;
            }
        }
        a.recycle();
    }

    private void init() {
        //圆圈画笔
        mCirclePaint = new Paint();
        mCirclePaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mCirclePaint.setAntiAlias(true);
        //线画笔
        mLinePaint = new Paint();
        mLinePaint.setAntiAlias(true);
        mLinePaint.setStrokeWidth(2);
        //文本画笔
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int height;
        if (MeasureSpec.EXACTLY == widthMode) {
            width = widthSize;
        } else {
            width = dpToPx(200);
            if (MeasureSpec.AT_MOST == widthMode) {
                width = Math.min(width, widthSize);
            }
        }
        if (MeasureSpec.EXACTLY == heightMode) {
            height = heightSize;
        } else {
            height = dpToPx(45);
            if (MeasureSpec.AT_MOST == heightMode) {
                height = Math.min(height, heightSize);
            }
        }
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        int width = getWidth() / data.length;
        //绘制上部文字
        drawTopText(canvas, data.length, width);
        canvas.save();
        //把画布向右移动 "已发货"的文本宽度 一半的距离(让第一个圆圈正对着该文本的中间)
        //同时向下移动30px,即在上部文字的下方30px处画圆圈和线
        canvas.translate(getTextWidth(data[0]) / 2, 30);
        //绘制圆圈和线
        for (int i = 0; i < data.length; i++) {
            if (i < pointPosition) {
                mCirclePaint.setColor(selectedCircleColor);
                mLinePaint.setColor(selectedLineColor);
            } else if (i == pointPosition) {
                mCirclePaint.setColor(selectedCircleColor);
                mLinePaint.setColor(Color.GRAY);
            } else {
                mCirclePaint.setColor(Color.GRAY);
                mLinePaint.setColor(Color.GRAY);
            }
            canvas.drawCircle(5 + getPaddingLeft() + i * width, 5 + getPaddingTop(), 5, mCirclePaint);
            //最后一个点之后不再画线
            if (i != data.length - 1) {
                canvas.drawLine(15 + getPaddingLeft() + i * width, 5 + getPaddingTop(),
                        getPaddingLeft() + (i + 1) * width - 5, 5 + getPaddingTop(), mLinePaint);
            }
        }
        //重置画布状态(恢复到上次save时的状态,即没有translate时的状态)
        canvas.restore();
        canvas.save();
        //再向下平移55px,即在圆圈和线的下方55px处绘制底部文字
        canvas.translate(0, 55);
        //绘制底部文字
        drawBottomText(canvas, data.length, width);
        //再次把画布的状态重置
        canvas.restore();
    }

    /**
     * 绘制上部文字
     */
    private void drawTopText(Canvas canvas, int length, int width) {
        for (int i = 0; i < length; i++) {
            if (i == pointPosition) {
                mTextPaint.setColor(selectedCircleColor);
                mTextPaint.setTextSize(16);
                //下面两行添加文字背景
//                mTextBackground.setBounds(getTextBound(i, width, data[i]));
//                mTextBackground.draw(canvas);
            } else {
                mTextPaint.setColor(Color.GRAY);
                mTextPaint.setTextSize(14);
            }
            canvas.drawText(data[i], 5 + getPaddingLeft() + i * width, getPaddingTop() + 20, mTextPaint);
        }
    }

    /**
     * 绘制底部文字
     */
    private void drawBottomText(Canvas canvas, int length, int width) {
        mTextPaint.setColor(Color.BLACK);
        if (!TextUtils.isEmpty(startPositionText)) {
            canvas.drawText(startPositionText, 5 + getPaddingLeft(), 5 + getPaddingTop(), mTextPaint);
        }
        if (!TextUtils.isEmpty(arrivePositionText)) {
            canvas.drawText(arrivePositionText, 5 + getPaddingLeft() + width * (length - 1), 5 + getPaddingTop(), mTextPaint);
        }
    }

    /**
     * 设置最新点的位置(从0开始)
     * 比如 pointPosition==2,则前3个点和2个线都是选中颜色,其他都是未选中颜色
     */
    public void setNewestPointPosition(int pointPosition) {
        if (data.length < pointPosition) {
            pointPosition = data.length - 1;
        }
        this.pointPosition = pointPosition;
        invalidate();
    }

    /**
     * 获取最新点的位置(从0开始)
     */
    public int getNewestPointPosition() {
        return pointPosition;
    }

    /**
     * 设置出发地
     */
    public void setStartPositionText(String startPositionText) {
        this.startPositionText = startPositionText;
        invalidate();
    }

    /**
     * 获取出发地
     */
    public String getStartPositionText() {
        return startPositionText;
    }

    /**
     * 设置目的地
     */
    public void setArrivePositionText(String arrivePositionText) {
        this.arrivePositionText = arrivePositionText;
        invalidate();
    }

    /**
     * 获取目的地
     */
    public String getArrivePositionText() {
        return arrivePositionText;
    }

    /**
     * 设置选中的圆圈颜色
     */
    public void setSelectedCircleColor(int color) {
        this.selectedCircleColor = color;
        invalidate();
    }
    /**
     * 设置选中的线的颜色
     */
    public void setSelectedLineColor(int color) {
        this.selectedLineColor = color;
        invalidate();
    }

    private int getTextWidth(String text) {
        Rect textBound = new Rect();
        mTextPaint.getTextBounds(text, 0, text.length(), textBound);
        return textBound.width();
    }

    private Rect getTextBound(int i, int width, String text) {
        Rect textBound = new Rect();
        mTextPaint.getTextBounds(text, 0, text.length(), textBound);
        Rect rect = new Rect(getPaddingLeft() + i * width - 5, getPaddingTop() - 15,
                15 + getPaddingLeft() + i * width + textBound.width(), getPaddingTop() + 30 + textBound.height());
        return rect;
    }

    /**
     * dp转px
     */
    public  int dpToPx(int dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
                dp, getContext().getResources().getDisplayMetrics());
    }
}


代码就这么多,注释也很详细,有需要的直接拿走不谢.

下面再说一下如何绘制背景,如文章开头所说,不是没有背景,而是背景太难看了,不好意思展示出来.

其实一开始我也不是太清楚如何绘制背景,因为这个背景不是全局的,而是被选中文字的背景,并且这个背景还是跟着选中的文字走的,选中的文字在哪,背景就在哪,一开始有点思路,就是我知道如何做到选中的文字在哪,背景就在哪,但是却不是太清楚如何绘制这个背景,不过当我研究了TextView绘制背景的源码之后就知道如何绘制了(TextView中其实没有绘制背景的代码.代码在他的父类View中有).

现在想来绘制背景和绘制圆圈其实本质都一样,我目前知道的有两种方式.

1,采用Drawable ,这个方式我是研究View源码知道的,Drawable也有一个draw方法,用来绘制他自己的,但是需要一个绘制范围,如draw方法的javadoc中所说,使用Drawable的draw方法之前,需要先调用setBound()方法,而setBound方法其实就是来确定Drawable的绘制范围的.使用Drawable比较灵活,因为Drawable不仅可以是图片,也可以是xml,你甚至也可以把颜色转为Drawable使用.

2,使用Paint,这个也是一种方式,并且实现起来也很简单,我之前把绘制背景想的太高深复杂了,导致一开始没想到这种方式,其实不管使用paint还是Drawable,都需要一个绘制范围,不然怎么知道在哪绘制呢,所以难点不在于绘制,而是如何确定绘制的范围.

绘制范围的确定:

.绘制范围的确定需要根据具体的控件样式来确定,以本文的view来说,我要绘制被选中的文本的背景,我需要知道文本绘制的起点X坐标以及文本的宽度,这是用来确定文本背景的宽度,当然你可以为了好看再主动加点padding值,同样的,我也需要知道文本绘制的起点左上角的Y坐标以及文本的高度,用来确定背景的高度,当然也可以加点padding值为了好看,根据以上的几个值就可以设置绘制范围了,其实就是一个Rect对象或RectF对象,把左,上,右,下的坐标设置进去即可,对于Drawable来说,调用setBound方法,把Rect放进去,然后再调用Drawable的draw方法就可以在指定范围绘制背景了.

说了这么多,本文的这个view的文字背景绘制起来其实还有点小问题,主要是因为在获取文字宽高的时候,由于文字的大小会时刻的变,导致背景看起来很丑,所以就没展示出来.


  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
一款简单的物流状态进度展示自定义View,仅供参考学习效果图使用方法布局文件<ExpressView         android:id="@ id/expressview"         android:layout_width="match_parent"         android:layout_height="match_parent"         express:circleToTextMargin="12dp"         express:expressCircleOuterRadius="8dp"         express:expressCircleRadius="6dp"         express:expressTextMargin="12dp"         express:expressTextSize="14sp"         express:expressTextVecPadding="5dp"         express:expressTimeTextSize="10sp"         express:firstExpressCircleMarginLeft="16dp"         express:firstExpressCircleMarginTop="16dp"         express:isTimeButtonVisible="true" />控件属性介绍firstExpressCircleMarginLeft 第一个物流状态点距离父控件坐边的间距 firstExpressCircleMarginTop 第一个物流状态点距离父控件上边的间距 expressCircleRadius 物流状态点内圈半径 expressCircleOuterRadius 物流状态点外圈半径 circleToTextMargin 物流状态提示圈到文字背景的距离 expressTextMargin 文字距离背景边距 expressTextVecPadding 每个物流信息竖直方向的间距 expressTextSize 文字大小 expressTimeTextSize 时间文字大小 isTimeButtonVisible 是否显示时间和文字按钮客户端        //数据源         final List<ExpressMessageBean> list = new ArrayList<>();        ExpressMessageBean bean = new ExpressMessageBean();         bean.setFlowState(1);         bean.setFlowStateBtRight("购买流程");         bean.setCreateTime(1487259871184l);         bean.setCreateTimeFormat(TimeUtils.millis2String(1487259871184l));         bean.setOpContent("您已付款0.1200元,购买 地下城与勇士/广东区/广东1区帐号,请联系卖家卡罗特将密保手机绑定您的手机号 189****2298");         list.add(bean);         bean = new ExpressMessageBean();         bean.setFlowState(4);         bean.setFlowStateBtLeft("同意退款"); //设置左右按钮文字         bean.setFlowStateBtRight("拒绝退款");         bean.setCreateTime(1487259991260l);         bean.setCreateTimeFormat(TimeUtils.millis2String(1487259991260l));         bean.setOpContent("天空套 0.1200 1个-申请退款");         list.add(bean);         bean = new ExpressMessageBean();         bean.setFlowState(5);         bean.setCreateTime(1487259871184l);         bean.setCreateTimeFormat(TimeUtils.millis2String(1487259871184l));         bean.setOpContent("您已付款0.1200元,购买 地下城与勇士/广东区/广东1区帐号,请联系卖家卡罗特将密保手机绑定您的手机号 189****2298");         list.add(bean);         bean = new ExpressMessageBean();         bean.setFlowState(1);         bean.setFlowStateBtRight("购买流程"); //设置右按钮文字         bean.setCreateTime(1487259991260l);         bean.setCreateTimeFormat(TimeUtils.millis2String(1487259991260l));         bean.setOpContent("天空套 0.1200 1个-申请退款");         list.add(bean);        //数据源适配         ExpressViewAdapter adapter = new ExpressViewAdapter<ExpressMessageBean>(list) {            @Override             public ExpressViewData bindData(ExpressView expressView, int position, ExpressMessageBean expressMessageBean) {                ExpressViewData data = new ExpressViewData();                 data.setContent(expressMessageBean.getOpContent());                 data.setTime(expressMessageBean.getCreateTimeFormat());                 data.setLeftBtnText(expressMessageBean.getFlowStateBtLeft());                 data.setRightBtnText(expressMessageBean.getFlowStateBtRight());                return data;             }         };         expressView.setAdapter(adapter);         adapter.notifyDataChanged();        //处理点击事件         expressView.setOnExpressItemButtonClickListener(new ExpressView.OnExpressItemButtonClickListener() {            @Override             public void onExpressItemButtonClick(int position, int status) {                switch (list.get(position).getFlowState()){                    case 1:                         if(status == 1){ //购买流程                             ToastUtil.ToastBottow(TestActivity.this, list.get(position).getFlowStateBtRight());                         }                        break;                    case 4:                         if(status == 0) { //同意退款                             ToastUtil.ToastBottow(TestActivity.this, list.get(position).getFlowStateBtLeft());                         } else if(status == 1){ //拒绝退款                             ToastUtil.ToastBottow(TestActivity.this, list.get(position).getFlowStateBtRight());                         }                        break;                    default:                         break;                 }             }         });待完善1、处理滑动冲突2、处理滑动到顶部和到底部停止滑动的逻辑3、实现弹性滑动的效果博客文章介绍http://www.jianshu.com/p/2d87f62d5d27

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值