Android自定义View实现垂直时间轴布局

发布时间 - 2026-01-11 00:05:35    点击率:

时间轴,顾名思义就是将发生的事件按照时间顺序罗列起来,给用户带来一种更加直观的体验。京东和淘宝的物流顺序就是一个时间轴,想必大家都不陌生,如下图:

分析

实现这个最常用的一个方法就是用ListView,我这里用继承LinearLayout的方式来实现。首先定义了一些自定义属性:

attrs.xml

<?xml version="1.0" encoding="utf-8"?> 
<resources> 
 <declare-styleable name="TimelineLayout"> 
  <!--时间轴左偏移值--> 
  <attr name="line_margin_left" format="dimension"/> 
  <!--时间轴上偏移值--> 
  <attr name="line_margin_top" format="dimension"/> 
  <!--线宽--> 
  <attr name="line_stroke_width" format="dimension"/> 
  <!--线的颜色--> 
  <attr name="line_color" format="color"/> 
  <!--点的大小--> 
  <attr name="point_size" format="dimension"/> 
  <!--点的颜色--> 
  <attr name="point_color" format="color"/> 
  <!--图标--> 
  <attr name="icon_src" format="reference"/> 
 </declare-styleable> 
</resources> 

TimelineLayout.java

package com.jackie.timeline; 
 
import android.content.Context; 
import android.content.res.TypedArray; 
import android.graphics.Bitmap; 
import android.graphics.Canvas; 
import android.graphics.Paint; 
import android.graphics.drawable.BitmapDrawable; 
import android.support.annotation.Nullable; 
import android.util.AttributeSet; 
import android.view.View; 
import android.widget.LinearLayout; 
 
/** 
 * Created by Jackie on 2017/3/8. 
 * 时间轴控件 
 */ 
 
public class TimelineLayout extends LinearLayout { 
 private Context mContext; 
 
 private int mLineMarginLeft; 
 private int mLineMarginTop; 
 private int mLineStrokeWidth; 
 private int mLineColor;; 
 private int mPointSize; 
 private int mPointColor; 
 private Bitmap mIcon; 
 
 private Paint mLinePaint; //线的画笔 
 private Paint mPointPaint; //点的画笔 
  
 
 //第一个点的位置 
 private int mFirstX; 
 private int mFirstY; 
 //最后一个图标的位置 
 private int mLastX; 
 private int mLastY; 
 
 public TimelineLayout(Context context) { 
  this(context, null); 
 } 
 
 public TimelineLayout(Context context, @Nullable AttributeSet attrs) { 
  this(context, attrs, 0); 
 } 
 
 public TimelineLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 
  super(context, attrs, defStyleAttr); 
  TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TimelineLayout); 
  mLineMarginLeft = ta.getDimensionPixelOffset(R.styleable.TimelineLayout_line_margin_left, 10); 
  mLineMarginTop = ta.getDimensionPixelOffset(R.styleable.TimelineLayout_line_margin_top, 0); 
  mLineStrokeWidth = ta.getDimensionPixelOffset(R.styleable.TimelineLayout_line_stroke_width, 2); 
  mLineColor = ta.getColor(R.styleable.TimelineLayout_line_color, 0xff3dd1a5); 
  mPointSize = ta.getDimensionPixelSize(R.styleable.TimelineLayout_point_size, 8); 
  mPointColor = ta.getDimensionPixelOffset(R.styleable.TimelineLayout_point_color, 0xff3dd1a5); 
 
  int iconRes = ta.getResourceId(R.styleable.TimelineLayout_icon_src, R.drawable.ic_ok); 
  BitmapDrawable drawable = (BitmapDrawable) context.getResources().getDrawable(iconRes); 
  if (drawable != null) { 
   mIcon = drawable.getBitmap(); 
  } 
 
  ta.recycle(); 
 
  setWillNotDraw(false); 
  initView(context); 
 } 
 
 private void initView(Context context) { 
  this.mContext = context; 
 
  mLinePaint = new Paint(); 
  mLinePaint.setAntiAlias(true); 
  mLinePaint.setDither(true); 
  mLinePaint.setColor(mLineColor); 
  mLinePaint.setStrokeWidth(mLineStrokeWidth); 
  mLinePaint.setStyle(Paint.Style.FILL_AND_STROKE); 
 
  mPointPaint = new Paint(); 
  mPointPaint.setAntiAlias(true); 
  mPointPaint.setDither(true); 
  mPointPaint.setColor(mPointColor); 
  mPointPaint.setStyle(Paint.Style.FILL); 
 } 
 
 @Override 
 protected void onDraw(Canvas canvas) { 
  super.onDraw(canvas); 
   
  drawTimeline(canvas); 
 } 
 
 private void drawTimeline(Canvas canvas) { 
  int childCount = getChildCount(); 
 
  if (childCount > 0) { 
   if (childCount > 1) { 
    //大于1,证明至少有2个,也就是第一个和第二个之间连成线,第一个和最后一个分别有点和icon 
    drawFirstPoint(canvas); 
    drawLastIcon(canvas); 
    drawBetweenLine(canvas); 
   } else if (childCount == 1) { 
    drawFirstPoint(canvas); 
   } 
  } 
 } 
 
 private void drawFirstPoint(Canvas canvas) { 
  View child = getChildAt(0); 
  if (child != null) { 
   int top = child.getTop(); 
   mFirstX = mLineMarginLeft; 
   mFirstY = top + child.getPaddingTop() + mLineMarginTop; 
 
   //画圆 
   canvas.drawCircle(mFirstX, mFirstY, mPointSize, mPointPaint); 
  } 
 } 
 
 private void drawLastIcon(Canvas canvas) { 
  View child = getChildAt(getChildCount() - 1); 
  if (child != null) { 
   int top = child.getTop(); 
   mLastX = mLineMarginLeft; 
   mLastY = top + child.getPaddingTop() + mLineMarginTop; 
 
   //画图 
   canvas.drawBitmap(mIcon, mLastX - (mIcon.getWidth() >> 1), mLastY, null); 
  } 
 } 
 
 private void drawBetweenLine(Canvas canvas) { 
  //从开始的点到最后的图标之间,画一条线 
  canvas.drawLine(mFirstX, mFirstY, mLastX, mLastY, mLinePaint); 
  for (int i = 0; i < getChildCount() - 1; i++) { 
   //画圆 
   int top = getChildAt(i).getTop(); 
   int y = top + getChildAt(i).getPaddingTop() + mLineMarginTop; 
   canvas.drawCircle(mFirstX, y, mPointSize, mPointPaint); 
  } 
 } 
 
 public int getLineMarginLeft() { 
  return mLineMarginLeft; 
 } 
 
 public void setLineMarginLeft(int lineMarginLeft) { 
  this.mLineMarginLeft = lineMarginLeft; 
  invalidate(); 
 } 
} 

从上面的代码可以看出,分三步绘制,首先绘制开始的实心圆,然后绘制结束的图标,然后在开始和结束之间先绘制一条线,然后在线上在绘制每个步骤的实心圆。
activity_main.xml

<?xml version="1.0" encoding="utf-8"?> 
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
 xmlns:app="http://schemas.android.com/apk/res-auto" 
 android:layout_width="match_parent" 
 android:layout_height="match_parent" 
 android:orientation="vertical"> 
 
 <LinearLayout 
  android:layout_width="match_parent" 
  android:layout_height="50dp" 
  android:weightSum="2"> 
 
  <Button 
   android:id="@+id/add_item" 
   android:layout_width="0dp" 
   android:layout_height="match_parent" 
   android:layout_weight="1" 
   android:text="add"/> 
 
  <Button 
   android:id="@+id/sub_item" 
   android:layout_width="0dp" 
   android:layout_height="match_parent" 
   android:layout_weight="1" 
   android:text="sub"/> 
 </LinearLayout> 
 
 <LinearLayout 
  android:layout_width="match_parent" 
  android:layout_height="wrap_content" 
  android:orientation="horizontal" 
  android:weightSum="2"> 
 
  <Button 
   android:id="@+id/add_margin" 
   android:layout_width="0dp" 
   android:layout_weight="1" 
   android:layout_height="wrap_content" 
   android:text="+"/> 
 
  <Button 
   android:id="@+id/sub_margin" 
   android:layout_width="0dp" 
   android:layout_weight="1" 
   android:layout_height="wrap_content" 
   android:text="-"/> 
 </LinearLayout> 
 
 <TextView 
  android:id="@+id/current_margin" 
  android:layout_width="match_parent" 
  android:layout_height="40dp" 
  android:gravity="center" 
  android:text="current line margin left is 25dp"/> 
 
 <ScrollView 
  android:layout_width="match_parent" 
  android:layout_height="wrap_content" 
  android:scrollbars="none"> 
 
  <com.jackie.timeline.TimelineLayout 
   android:id="@+id/timeline_layout" 
   android:layout_width="match_parent" 
   android:layout_height="wrap_content" 
   app:line_margin_left="25dp" 
   app:line_margin_top="8dp" 
   android:orientation="vertical" 
   android:background="@android:color/white"> 
  </com.jackie.timeline.TimelineLayout> 
 </ScrollView> 
</LinearLayout> 

MainActivity.java

package com.jackie.timeline; 
 
import android.os.Bundle; 
import android.support.v7.app.AppCompatActivity; 
import android.view.LayoutInflater; 
import android.view.View; 
import android.widget.Button; 
import android.widget.TextView; 
 
public class MainActivity extends AppCompatActivity implements View.OnClickListener { 
 private Button addItemButton; 
 private Button subItemButton; 
 private Button addMarginButton; 
 private Button subMarginButton; 
 private TextView mCurrentMargin; 
 
 private TimelineLayout mTimelineLayout; 
 
 @Override 
 protected void onCreate(Bundle savedInstanceState) { 
  super.onCreate(savedInstanceState); 
  setContentView(R.layout.activity_main); 
 
  initView(); 
 } 
 
 private void initView() { 
  addItemButton = (Button) findViewById(R.id.add_item); 
  subItemButton = (Button) findViewById(R.id.sub_item); 
  addMarginButton= (Button) findViewById(R.id.add_margin); 
  subMarginButton= (Button) findViewById(R.id.sub_margin); 
  mCurrentMargin= (TextView) findViewById(R.id.current_margin); 
  mTimelineLayout = (TimelineLayout) findViewById(R.id.timeline_layout); 
 
  addItemButton.setOnClickListener(this); 
  subItemButton.setOnClickListener(this); 
  addMarginButton.setOnClickListener(this); 
  subMarginButton.setOnClickListener(this); 
 } 
 
 private int index = 0; 
 private void addItem() { 
  View view = LayoutInflater.from(this).inflate(R.layout.item_timeline, mTimelineLayout, false); 
  ((TextView) view.findViewById(R.id.tv_action)).setText("步骤" + index); 
  ((TextView) view.findViewById(R.id.tv_action_time)).setText("2017年3月8日16:55:04"); 
  ((TextView) view.findViewById(R.id.tv_action_status)).setText("完成"); 
  mTimelineLayout.addView(view); 
  index++; 
 } 
 
 private void subItem() { 
  if (mTimelineLayout.getChildCount() > 0) { 
   mTimelineLayout.removeViews(mTimelineLayout.getChildCount() - 1, 1); 
   index--; 
  } 
 } 
 
 @Override 
 public void onClick(View v) { 
  switch (v.getId()){ 
   case R.id.add_item: 
    addItem(); 
    break; 
   case R.id.sub_item: 
    subItem(); 
    break; 
   case R.id.add_margin: 
    int currentMargin = UIHelper.pxToDip(this, mTimelineLayout.getLineMarginLeft()); 
    mTimelineLayout.setLineMarginLeft(UIHelper.dipToPx(this, ++currentMargin)); 
    mCurrentMargin.setText("current line margin left is " + currentMargin + "dp"); 
    break; 
   case R.id.sub_margin: 
    currentMargin = UIHelper.pxToDip(this, mTimelineLayout.getLineMarginLeft()); 
    mTimelineLayout.setLineMarginLeft(UIHelper.dipToPx(this, --currentMargin)); 
    mCurrentMargin.setText("current line margin left is " + currentMargin + "dp"); 
    break; 
   default: 
    break; 
  } 
 } 
} 

item_timeline.xml

<?xml version="1.0" encoding="utf-8"?> 
<RelativeLayout 
 xmlns:android="http://schemas.android.com/apk/res/android" 
 android:layout_width="match_parent" 
 android:layout_height="wrap_content" 
 android:paddingLeft="65dp" 
 android:paddingTop="20dp" 
 android:paddingRight="20dp" 
 android:paddingBottom="20dp"> 
 
 <TextView 
  android:id="@+id/tv_action" 
  android:layout_width="wrap_content" 
  android:layout_height="wrap_content" 
  android:textSize="14sp" 
  android:textColor="#1a1a1a" 
  android:text="测试一"/> 
 
 <TextView 
  android:id="@+id/tv_action_time" 
  android:layout_width="wrap_content" 
  android:layout_height="wrap_content" 
  android:textSize="12sp" 
  android:textColor="#8e8e8e" 
  android:layout_below="@id/tv_action" 
  android:layout_marginTop="10dp" 
  android:text="2017年3月8日16:49:12"/> 
 
 <TextView 
  android:id="@+id/tv_action_status" 
  android:layout_width="wrap_content" 
  android:layout_height="wrap_content" 
  android:textSize="14sp" 
  android:textColor="#3dd1a5" 
  android:layout_alignParentRight="true" 
  android:text="完成"/> 
 
</RelativeLayout> 

附上像素工具转化的工具类:

package com.jackie.timeline; 
 
import android.content.Context; 
 
/** 
 * Created by Jackie on 2017/3/8. 
 */ 
public final class UIHelper { 
 
 private UIHelper() throws InstantiationException { 
  throw new InstantiationException("This class is not for instantiation"); 
 } 
 
 /** 
  * dip转px 
  */ 
 public static int dipToPx(Context context, float dip) { 
  return (int) (dip * context.getResources().getDisplayMetrics().density + 0.5f); 
 } 
 
 /** 
  * px转dip 
  */ 
 public static int pxToDip(Context context, float pxValue) { 
  final float scale = context.getResources().getDisplayMetrics().density; 
  return (int) (pxValue / scale + 0.5f); 
 } 
} 

效果图如下:

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。


# Android  # View  # 时间轴  # android自定义控件实现简易时间轴(2)  # Android recyclerview实现纵向虚线时间轴的示例代码  # Android自定义控件实现时间轴  # Android使用自定义View实现横行时间轴效果  # Android自定义view仿淘宝快递物流信息时间轴  # Android实现快递物流时间轴效果  # Android实现列表时间轴  # Android自定义指示器时间轴效果实例代码详解  # 教你3分钟了解Android 简易时间轴的实现方法  # android自定义控件实现简易时间轴(1)  # 第一个  # 都不  # 我这  # 第二个  # 线上  # 自定义  # 可以看出  # 顾名思义  # 淘宝  # 来实现  # 点到  # 最常用  # 大家多多  # 如下图  # 一条线  # 画一  # 里用  # 京东  # 分三步  # 连成线 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: ,在苏州找工作,上哪个网站比较好?  Thinkphp 中 distinct 的用法解析  Laravel用户认证怎么做_Laravel Breeze脚手架快速实现登录注册功能  如何在宝塔面板中创建新站点?  Laravel怎么使用artisan命令缓存配置和视图  laravel怎么使用数据库工厂(Factory)生成带有关联模型的数据_laravel Factory生成关联数据方法  制作ppt免费网站有哪些,有哪些比较好的ppt模板下载网站?  Laravel如何处理CORS跨域请求?(配置示例)  如何确认建站备案号应放置的具体位置?  东莞专业网站制作公司有哪些,东莞招聘网站哪个好?  Python自动化办公教程_ExcelWordPDF批量处理案例  Python文件异常处理策略_健壮性说明【指导】  iOS中将个别页面强制横屏其他页面竖屏  电商网站制作多少钱一个,电子商务公司的网站制作费用计入什么科目?  如何用腾讯建站主机快速创建免费网站?  UC浏览器如何切换小说阅读源_UC浏览器阅读源切换【方法】  香港服务器租用每月最低只需15元?  Laravel控制器是什么_Laravel MVC架构中Controller的作用与实践  如何自定义建站之星网站的导航菜单样式?  laravel怎么用DB facade执行原生SQL查询_laravel DB facade原生SQL执行方法  lovemo网页版地址 lovemo官网手机登录  Laravel队列由Redis驱动怎么配置_Laravel Redis队列使用教程  如何自定义建站之星模板颜色并下载新样式?  如何在浏览器中启用Flash_2025年继续使用Flash Player的方法【过时】  Python自然语言搜索引擎项目教程_倒排索引查询优化案例  javascript中数组(Array)对象和字符串(String)对象的常用方法总结  Laravel如何实现本地化和多语言支持?(i18n教程)  制作无缝贴图网站有哪些,3dmax无缝贴图怎么调?  Linux系统命令中tree命令详解  Microsoft Edge如何解决网页加载问题 Edge浏览器加载问题修复  Android 常见的图片加载框架详细介绍  宙斯浏览器怎么屏蔽图片浏览 节省手机流量使用设置方法  Laravel如何使用Blade模板引擎?(完整语法和示例)  PHP怎么接收前端传的文件路径_处理文件路径参数接收方法【汇总】  Laravel如何配置和使用队列处理异步任务_Laravel队列驱动与任务分发实例  Laravel如何使用Telescope进行调试?(安装和使用教程)  Laravel怎么判断请求类型_Laravel Request isMethod用法  韩国服务器如何优化跨境访问实现高效连接?  如何用PHP快速搭建CMS系统?  如何登录建站主机?访问步骤全解析  ,怎么在广州志愿者网站注册?  Laravel如何为API生成Swagger或OpenAPI文档  如何在 Telegram Web View(iOS)中防止键盘遮挡底部输入框  VIVO手机上del键无效OnKeyListener不响应的原因及解决方法  Laravel怎么实现API接口鉴权_Laravel Sanctum令牌生成与请求验证【教程】  佐糖AI抠图怎样调整抠图精度_佐糖AI精度调整与放大细化操作【攻略】  如何用VPS主机快速搭建个人网站?  QQ浏览器网页版登录入口 个人中心在线进入  Laravel怎么使用Blade模板引擎_Laravel模板继承与Component组件复用【手册】  Laravel怎么集成Log日志记录_Laravel单文件与每日日志配置及自定义通道【详解】