仿网易严选物流界面

最终效果

最终效果

环境

AndroidStudio 3.1.2
compileSdkVersion 27

原理分析

1 从效果图可以看出,每条物流信息基本都是重复的布局(左侧稍有区别),因此我们只需要自定义物流条目即可,无需把整个效果作为自定义View.

2 最容易想到的方式就是直接利用 RecyclerView 实现.我们只要控制 Item 改变左侧样式即可.

3 本文利用自定义 View 继承 RelativeLayout 实现,暂时叫做 LogisticsLayout
Item

4 上图是一个自定义 LogisticsLayout , 因为继承自 RelativeLayout 所以右边的两行展示可以直接写在布局的 xml 中,然后距离左侧留出一定的距离,这样 LogisticsLayoutonLayout() 阶段会自动完成子孩子的绘制,我们只需要在 onDraw() 阶段绘制左侧圆点和虚线即可.这里需要注意的是 Item 之间是没有 divier 的,上图下方的空白是 Item 自身的 padding 或者 margin 属性. 这两个属性占据的高度是 Item 的高度的一部分.

5 绘制左侧效果时,区分三种情况,首部,普通,尾部.

首部 普通 尾部
颜色红色 颜色灰色 颜色灰色
虚线从圆点到bottom 虚线占据整个高度 虚线从top至圆点

6 条目中的电话高亮以及点击后自动拨号,我们直接使用 TextViewandroid:autoLink="phone"属性

效果实现

attr属性

定义一个可在xml使用的属性,用于限定左侧宽度. 这样做是为了方便维护.当然你也可以直接写死在 View 中.

在 values 文件夹下创建 attrs.xml 并写入下面代码

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="LogisticsLayout">
        <attr name="left_margin" format="dimension" />
    </declare-styleable>
</resources>

这里只定义一个 left_margin 即可

LogisticsLayout 实现


public class LogisticsLayout extends RelativeLayout {
    Paint linePaint;
    Paint circlePaint;
    // 左侧绘制范围的宽度,此值作为基准,
    float totalWidth;
    float totalHeight;
    float radius;
    float centerX;
    float marginTop;
    float dottedLen;

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

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

    public LogisticsLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.LogisticsLayout);
        try {
            totalWidth = array.getDimension(R.styleable.LogisticsLayout_left_margin, 60);
            centerX = totalWidth * 0.5f;
            radius = totalWidth * 0.1f;
            marginTop = totalWidth * 0.3f;
            dottedLen = totalWidth / 15;
        } finally {
            array.recycle();
        }
        init();
    }

    private void init() {
        linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        linePaint.setStyle(Paint.Style.FILL);
        linePaint.setStrokeWidth(3);
        linePaint.setColor(Color.GRAY);

        circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        circlePaint.setStyle(Paint.Style.FILL);
        circlePaint.setColor(Color.GRAY);

        // 清除标记,因为 ViewGroup 作为容器,默认不会触发 onDraw() 方法
        setWillNotDraw(false);
        //关闭硬件加速 这里非常重要, 不然虚线等没有效果
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        totalHeight = getHeight();
        System.out.println(totalWidth + " " + totalHeight + "hp");
        // 画虚线
        drawLine(canvas);
        // 画圆点
        drawCircle(canvas);
    }

    private void drawLine(Canvas canvas) {
        linePaint.setPathEffect(new DashPathEffect(new float[]{dottedLen, dottedLen}, 2));
        canvas.drawLine(centerX, 0, centerX, totalHeight, linePaint);
    }

    private void drawCircle(Canvas canvas) {
        canvas.drawCircle(centerX, marginTop, radius, circlePaint);
    }
}
注意几点
初始化时定义关键参数,其中以左侧宽度为基准,根据比例定义出 centerX, radius 等.这样做有助于不同屏幕的适配
画虚线和圆点,这部分比较简单,只需要第一步定义好位置参数,作图代码只需三行.
init() 方法中最后两行需要注意. 其中为了实现 ViewGroup 的 onDraw() 方法调用也可以采用在 xml 中设置背景的方式.
关闭硬件加速是为了实现某些特殊效果,例如虚线,阴影等

初步效果

LogisticsLayout 为布局跟节点,配合 RecyclerView 展示初步效果
Item布局:

<?xml version="1.0" encoding="utf-8"?>
<com.example.hepan.logistics.LogisticsLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:left_margin="50dp">

    <TextView
        android:id="@+id/tvTime"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="50dp"
        android:autoLink="phone"
        android:text="时间" />

    <TextView
        android:id="@+id/tvDesc"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/tvTime"
        android:layout_marginBottom="20dp"
        android:layout_marginLeft="50dp"
        android:autoLink="phone"
        android:text="描述" />
</com.example.hepan.logistics.LogisticsLayout>

结合 RecyclerView 效果
这里写图片描述

最终实现

上面的效果距离目标还有一定的距离.我们需要给 LogisticsLayout 添加判定类型类型的方法,以便在绘图时有所区别
1 在末尾添加 state 属性,并需改 drawLine drawCircle 方法

private void drawLine(Canvas canvas) {
        linePaint.setPathEffect(new DashPathEffect(new float[]{dottedLen, dottedLen}, 2));
        if (state == State.STATE_HEADER) {
            canvas.drawLine(centerX, marginTop, centerX, totalHeight, linePaint);
        } else if (state == State.STATE_FOOTER) {
            canvas.drawLine(centerX, 0, centerX, marginTop, linePaint);
        } else {
            canvas.drawLine(centerX, 0, centerX, totalHeight, linePaint);
        }
    }

    private void drawCircle(Canvas canvas) {
        if (state == State.STATE_HEADER) {
            circlePaint.setColor(Color.RED);
            canvas.drawCircle(centerX, marginTop, radius, circlePaint);
        } else {
            canvas.drawCircle(centerX, marginTop, radius, circlePaint);
        }
    }

    @State
    private int state = State.STATE_NORMAL;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({State.STATE_HEADER, State.STATE_NORMAL, State.STATE_FOOTER})
    public @interface State {
        int STATE_HEADER = 0;
        int STATE_NORMAL = 1;
        int STATE_FOOTER = 2;
    }

    public int getState() {
        return state;
    }

    public void setState(int state) {
        this.state = state;
    }

2 在适配器中设置属性

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        when (position) {
            0 -> {
                //设置属性
                holder.itemView.root.state = LogisticsLayout.State.STATE_HEADER
                //字体颜色
                holder.itemView.tvTime.setTextColor(Color.RED)
                holder.itemView.tvDesc.setTextColor(Color.RED)
            }
            data.size - 1 -> holder.itemView.root.state = LogisticsLayout.State.STATE_FOOTER
            else -> holder.itemView.root.state = LogisticsLayout.State.STATE_NORMAL
        }
        holder.itemView.tvDesc.text = data[position].split("\n")[0]
        holder.itemView.tvTime.text = data[position].split("\n")[1]
    }
修改后效果
这里写图片描述
注意几点
我这里用的 kotlin, when() 其实就是switch() 语句, .() 就是 set**()
onBindViewHolder 周期中 View 还未真正绘制,此处设置属性时不用内部调用 postInvalidate(); 等方法
用字符串模拟了条目的物流信息,其中用\n隔开
电话的颜色显示是以 colors.xml 文件中 <color name="colorAccent">****</color> 为默认值的.也就是app的强调色,如果想改变的话可以自定义 TextView 样式设置对应属性值.

备注

感谢何天鹏组长,本文思路是参考其代码完成的

项目地址 希望能有所帮助

猜你喜欢

转载自blog.csdn.net/hepann44/article/details/80695461
今日推荐