乐趣区

Android自定义View:快递时间轴实现

前言
在 Android 开发中,时间轴的 UI 非常常见,如下图:

储备知识:
1. 自定义 view 基础 2.RecyclerView 的使用 3. 自定义 RecyclerView.ItemDecoration
具体实现
1. 最终效果如下:
2. 实现思路

使用 RecyclerView, 自定义 RecyclerView.ItemDecoration
复习 ItemDecoration 中 getItemOffsets() 方法, 重写 onDraw() 方法
实现 RecyclerView.Adapter, 绑定数据

3. 详细设计

4. 具体实现
引入 RecyclerView 依赖包
dependencies {
……….
api ‘com.android.support:recyclerview-v7:28.0.0’
}
在布局文件中使用
<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:app=”http://schemas.android.com/apk/res-auto”
xmlns:tools=”http://schemas.android.com/tools”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
tools:context=”.MainActivity”>

<android.support.v7.widget.RecyclerView
android:id=”@+id/my_recycler_view”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:scrollbars=”horizontal”
/>

</RelativeLayout>
设置 item 布局
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:orientation=”vertical”
>

<TextView
android:id=”@+id/item_title”
android:text=”New Text”
android:textSize=”15sp”
android:layout_marginLeft=”30dp”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />

<TextView
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”New Text”
android:layout_marginLeft=”30dp”
android:textSize=”15sp”
android:id=”@+id/item_text”
android:layout_below=”@+id/item_title”
/>

</LinearLayout>
实现 RecyclerView.Adapter
public class MyAdapter extends RecyclerView.Adapter {
private LayoutInflater inflater;
private ArrayList<HashMap<String,Object>> listitem;

// 构造函数, 传入数据
public MyAdapter(Context context,ArrayList<HashMap<String, Object>> listitem) {
this.inflater = LayoutInflater.from(context);
this.listitem = listitem;
}

class ViewHolder extends RecyclerView.ViewHolder{
private TextView title,text;

public ViewHolder(@NonNull View itemView) {
super(itemView);
title = itemView.findViewById(R.id.item_title);
text = itemView.findViewById(R.id.item_text);
}

public TextView getTitle() {
return title;
}

public TextView getText() {
return text;
}

}

@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
return new ViewHolder(inflater.inflate(R.layout.list_cell,null));
// 绑定 item 布局
}

@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
// 绑定数据到 ViewHolder
ViewHolder vh = (ViewHolder) viewHolder;
vh.title.setText((CharSequence) listitem.get(i).get(“ItemTitle”));
vh.text.setText((CharSequence) listitem.get(i).get(“ItemText”));
}

@Override
public int getItemCount() {
return listitem.size();
}
}
自定义 RecyclerView.ItemDecoration
public class DividerItemDecoration extends RecyclerView.ItemDecoration {

// 轴点画笔
private final Paint mPaint;
// 时分画笔
private final Paint mPaint1;
// 年月画笔
private final Paint mPaint2;
//itemView 左 上 偏移量
private int itemView_leftinterval;
private int itemView_topintervarl;
// 轴点半径
private int circle_radius;
private final Bitmap mIcon;

// 在构造函数里初始化需要属性
public DividerItemDecoration(Context context){
mPaint = new Paint();
mPaint.setColor(Color.RED);// 设置画笔颜色为红色

mPaint1 = new Paint();
mPaint1.setColor(Color.BLUE);
mPaint1.setTextSize(30);// 设置绘制字体大小

mPaint2 = new Paint();
mPaint2.setColor(Color.BLUE);
mPaint2.setTextSize(15);

itemView_leftinterval = 200; // 左偏移长度 200
itemView_topintervarl = 50; // 上偏移长度 50

circle_radius = 10;// 轴点半径为 10
mIcon = BitmapFactory.decodeResource(context.getResources(),R.mipmap.logo);

}

@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);

// 设置 itemview 的左上偏移量, 即为 onDraw 可绘制的区域
outRect.set(itemView_leftinterval,itemView_topintervarl,0,0);

}

@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);

// 获取 RecyclerView 的 Child 的个数
int childCount = parent.getChildCount();
// 遍历每个 item, 分别获取他们的位置信息, 然后在绘制对应的分割线
for (int i=0;i<childCount;i++){
View view = parent.getChildAt(i);// 获取每个 item 对象

/**
* 绘制轴点
*/
// 轴点 = 圆 = 圆心 (x,y)

float centerX = view.getLeft() – itemView_leftinterval/3;
float centerY = view.getTop() – itemView_topintervarl+(itemView_topintervarl+view.getHeight()/2);
// 绘制轴点圆
//c.drawCircle(centerX,centerY,circle_radius,mPaint);
c.drawBitmap(mIcon,centerX-circle_radius,centerY-circle_radius,mPaint);

/**
* 绘制上半轴线
*/
// 上端点坐标 (x,y)
float upLine_up_x = centerX;
float upLine_up_y =view.getTop()-itemView_topintervarl;

// 下端点坐标 (x,y)
float upLine_down_x = centerX;
float upLine_down_y = centerY-circle_radius;

c.drawLine(upLine_up_x,upLine_up_y,upLine_down_x,upLine_down_y,mPaint);// 绘制下半轴线

/**
* 绘制下半轴线
*/
// 上端点坐标 (x,y)
float bottomLine_up_x = centerX;
float bottom_up_y = centerY + circle_radius;

// 下端点坐标 (x,y)
float bottomLine_bottom_x = centerX;
float bottomLine_bottom_y = view.getBottom();

// 绘制下半部轴线
c.drawLine(bottomLine_up_x, bottom_up_y, bottomLine_bottom_x, bottomLine_bottom_y, mPaint);

/**
* 绘制左边时间文本
*/
int index = parent.getChildAdapterPosition(view);
// 绘制时间文本起始位置
float Text_x = view.getLeft()-itemView_leftinterval*5/6;
float Text_y = upLine_down_y;

// 根据 item 位置设置时间

switch (index){
case 0:
// 设置绘制日期
c.drawText(“13:40”,Text_x,Text_y,mPaint1);
c.drawText(“2018.4.03”,Text_x+5,Text_y+20,mPaint2);
break;
case 1:
// 设置绘制日期
c.drawText(“13:40”,Text_x,Text_y,mPaint1);
c.drawText(“2018.4.03”,Text_x+5,Text_y+20,mPaint2);
break;
case 2:
// 设置绘制日期
c.drawText(“13:40”,Text_x,Text_y,mPaint1);
c.drawText(“2018.4.03”,Text_x+5,Text_y+20,mPaint2);
break;
case 3:
// 设置绘制日期
c.drawText(“13:40”,Text_x,Text_y,mPaint1);
c.drawText(“2018.4.03”,Text_x+5,Text_y+20,mPaint2);
break;
case 4:
// 设置绘制日期
c.drawText(“13:40”,Text_x,Text_y,mPaint1);
c.drawText(“2018.4.03”,Text_x+5,Text_y+20,mPaint2);
break;
case 5:
// 设置绘制日期
c.drawText(“13:40”,Text_x,Text_y,mPaint1);
c.drawText(“2018.4.03”,Text_x+5,Text_y+20,mPaint2);
break;
default:
c.drawText(“ 已签收 ”,Text_x,Text_y,mPaint1);

}

}

}

}

初始化数据, 绑定 RecyclerView
public class MainActivity extends AppCompatActivity {

private ArrayList<HashMap<String, Object>> itemlist;
private RecyclerView rl;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
initView();
}

private void initData() {
itemlist = new ArrayList<HashMap<String, Object>>();
HashMap<String, Object> map1 = new HashMap<String, Object>();
HashMap<String, Object> map2 = new HashMap<String, Object>();
HashMap<String, Object> map3 = new HashMap<String, Object>();
HashMap<String, Object> map4 = new HashMap<String, Object>();
HashMap<String, Object> map5 = new HashMap<String, Object>();
HashMap<String, Object> map6 = new HashMap<String, Object>();

map1.put(“ItemTitle”, “ 中国广州公司已发出 ”);
map1.put(“ItemText”, “ 发件人: 妙卡迪文化公司 ”);
itemlist.add(map1);

map2.put(“ItemTitle”, “ 国际顺丰已收入 ”);
map2.put(“ItemText”, “ 等待中转 ”);
itemlist.add(map2);

map3.put(“ItemTitle”, “ 国际顺丰转件中 ”);
map3.put(“ItemText”, “ 下一站中国 ”);
itemlist.add(map3);

map4.put(“ItemTitle”, “ 中国顺丰已收入 ”);
map4.put(“ItemText”, “ 下一站江苏理工大学 ”);
itemlist.add(map4);

map5.put(“ItemTitle”, “ 中国顺丰派件中 ”);
map5.put(“ItemText”, “ 等待派件 ”);
itemlist.add(map5);

map6.put(“ItemTitle”, “ 江苏理工大学已签收 ”);
map6.put(“ItemText”, “ 收件人:darryrzhong”);
itemlist.add(map6);

}

private void initView() {
rl = findViewById(R.id.my_recycler_view);
LinearLayoutManager manager = new LinearLayoutManager(this);
rl.setLayoutManager(manager);
// 当知道 Adapter 内 Item 的改变不会影响 RecyclerView 宽高的时候,可以设置为 true 让 RecyclerView 避免重新计算大小。
rl.setHasFixedSize(true);
rl.addItemDecoration(new DividerItemDecoration(this));// 设置自定义分割线
MyAdapter adapter = new MyAdapter(this,itemlist);
rl.setAdapter(adapter);
}
}
至此, 自定义 RecyclerView 就实现完成了.
参考文章:
Android 自定义 View 实战系列:时间轴
欢迎关注作者 darryrzhong, 更多干货等你来拿哟.
请赏个小红心!因为你的鼓励是我写作的最大动力!
更多精彩文章请关注

个人博客:darryrzhong
掘金
简书
SegmentFault
慕课网手记

退出移动版