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单文件与每日日志配置及自定义通道【详解】

