本文主要是介绍仿网易严选物流界面,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
-
- 最终效果
- 环境
- 原理分析
- 效果实现
- attr属性
- LogisticsLayout 实现
- 初步效果
- 最终实现
最终效果
环境
AndroidStudio 3.1.2
compileSdkVersion 27
原理分析
1 从效果图可以看出,每条物流信息基本都是重复的布局(左侧稍有区别),因此我们只需要自定义物流条目即可,无需把整个效果作为自定义View.
2 最容易想到的方式就是直接利用 RecyclerView 实现.我们只要控制 Item 改变左侧样式即可.
3 本文利用自定义 View 继承 RelativeLayout 实现,暂时叫做 LogisticsLayout
4 上图是一个自定义 LogisticsLayout , 因为继承自 RelativeLayout 所以右边的两行展示可以直接写在布局的 xml 中,然后距离左侧留出一定的距离,这样 LogisticsLayout 在 onLayout()
阶段会自动完成子孩子的绘制,我们只需要在 onDraw()
阶段绘制左侧圆点和虚线即可.这里需要注意的是 Item 之间是没有 divier 的,上图下方的空白是 Item 自身的 padding 或者 margin 属性. 这两个属性占据的高度是 Item 的高度的一部分.
5 绘制左侧效果时,区分三种情况,首部,普通,尾部.
首部 | 普通 | 尾部 |
---|---|---|
颜色红色 | 颜色灰色 | 颜色灰色 |
虚线从圆点到bottom | 虚线占据整个高度 | 虚线从top至圆点 |
6 条目中的电话高亮以及点击后自动拨号,我们直接使用 TextView 的 android: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);}@Overrideprotected 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);}}@Stateprivate 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_FOOTERelse -> 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 样式设置对应属性值.
- 备注
-
感谢何天鹏组长,本文思路是参考其代码完成的
- 项目地址 希望能有所帮助
这篇关于仿网易严选物流界面的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!