Android实现时间轴 (附带源码)

一、项目概述

1.1 背景与意义

在许多应用场景中,我们需要展示一系列按时间先后排列的事件,比如:

  • 社交应用中的动态时间线

  • 订单状态跟踪界面的步骤展示

  • 项目日志或版本发布记录

一个直观的“时间轴”控件能够帮助用户快速了解事件的先后顺序及状态。Android 原生并不提供专门的时间轴控件,因此我们通常使用 RecyclerView + 自定义 ItemDecoration 或者在每个 item 中绘制线条和节点来实现这一效果。

1.2 功能需求

  • 垂直方向时间轴:左侧垂直一直线,线上若干节点(圆点);每条 item 水平分为“时间”与“内容”两部分。

  • 节点样式:第一条和最后一条可以自定义不同样式(如大圆、突出色);中间节点统一样式。

  • item 间隔:时间链自动延伸,首尾断点处理平滑;

  • 数据驱动:可动态加载任意长度的事件列表;

  • 样式可定制:线宽、圆点大小、颜色、左右间距、时间文字样式等可在代码或属性中配置。


二、相关技术知识

  1. RecyclerView

    • 支持动态列表、可复用 ViewHolder

    • RecyclerView.ItemDecoration 可用于在 item 绘制前后插入自定义绘制(如时间轴的线段和节点)。

  2. Canvas 绘图

    • Canvas.drawLine()Canvas.drawCircle() 实现线段与节点;

    • Paint 设置颜色、线宽、抗锯齿等属性。

  3. 自定义属性

    • 可在 res/values/attrs.xml 中声明自定义属性,如 timelineLineColortimelineDotRadius,在布局中通过 app: 前缀配置。

  4. 数据模型

    • 自定义 TimelineItem 包含 timecontent 字段;可扩展 status 字段以控制节点样式。


三、实现思路

  1. 数据准备
    构造一个 List<TimelineItem>,每个元素包含时间字符和内容描述。

  2. 布局设计

    • activity_main.xml:包含一个全屏 RecyclerView

    • item_timeline.xml:左右两列布局,左侧 TextView 显示时间,右侧 TextView 显示内容,并留出左侧绘制时间轴的区域。

  3. Adapter 实现

    • TimelineAdapter 继承自 RecyclerView.Adapter,使用单一 ViewType,在 onBindViewHolder() 中填充时间与内容。

  4. ItemDecoration 实现

    • onDrawOver() 中:

      • 计算每个 item 左侧中点的 Y 坐标;

      • 绘制上半段线(如果不是第一条)和下半段线(如果不是最后一条);

      • 在中点绘制圆点;

    • 线段和圆点的样式(颜色、宽度、半径)可在构造时通过参数传递。

  5. 集成与刷新

    • MainActivity 中初始化数据、适配器和 ItemDecoration

    • 数据更新后调用 adapter.notifyDataSetChanged() 即可刷新。


四、整合代码

4.1 Java 代码(MainActivity.java,所有类集中)

package com.example.timeline;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;

/**
 * MainActivity —— 时间轴示例
 */
public class MainActivity extends AppCompatActivity {

    private RecyclerView recyclerView;
    private TimelineAdapter adapter;
    private List<TimelineItem> dataList;

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

		// 1. 初始化数据
        dataList = new ArrayList<>();
        dataList.add(new TimelineItem("2023-01-01", "项目立项"));
        dataList.add(new TimelineItem("2023-02-15", "完成需求分析"));
        dataList.add(new TimelineItem("2023-04-10", "完成第一版设计"));
        dataList.add(new TimelineItem("2023-06-01", "开发与测试"));
        dataList.add(new TimelineItem("2023-07-20", "上线发布"));

		// 2. 初始化 RecyclerView 和 Adapter
        recyclerView = findViewById(R.id.recycler);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        adapter = new TimelineAdapter(this, dataList);
        recyclerView.setAdapter(adapter);

		// 3. 添加自定义 ItemDecoration 来绘制时间轴
        recyclerView.addItemDecoration(new TimelineItemDecoration(this, dataList.size()));
    }

    // ========================================================================
    // TimelineItem —— 数据模型
    // ========================================================================
    static class TimelineItem {
        String time, content;
        TimelineItem(String t, String c) { time = t; content = c; }
    }

    // ========================================================================
    // TimelineAdapter —— RecyclerView.Adapter
    // ========================================================================
    static class TimelineAdapter extends RecyclerView.Adapter<TimelineAdapter.TimelineVH> {
        private Context ctx;
        private List<TimelineItem> items;

        TimelineAdapter(Context c, List<TimelineItem> list) {
            ctx = c; items = list;
        }

        @NonNull @Override
        public TimelineVH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View v = LayoutInflater.from(ctx)
                     .inflate(R.layout.item_timeline, parent, false);
            return new TimelineVH(v);
        }

        @Override public void onBindViewHolder(@NonNull TimelineVH vh, int pos) {
            TimelineItem it = items.get(pos);
            vh.tvTime.setText(it.time);
            vh.tvContent.setText(it.content);
        }

        @Override public int getItemCount() { return items.size(); }

        static class TimelineVH extends RecyclerView.ViewHolder {
            TextView tvTime, tvContent;
            TimelineVH(View v) {
                super(v);
                tvTime    = v.findViewById(R.id.tv_time);
                tvContent = v.findViewById(R.id.tv_content);
            }
        }
    }

    // ========================================================================
    // TimelineItemDecoration —— 自定义 ItemDecoration 绘制时间轴
    // ========================================================================
    static class TimelineItemDecoration extends RecyclerView.ItemDecoration {
        private Paint linePaint, dotPaint;
        private int lineWidthPx, dotRadiusPx;
        private int itemCount;
        private int offset;
        TimelineItemDecoration(Context ctx, int totalCount) {
            itemCount = totalCount;
            DisplayMetrics dm = ctx.getResources().getDisplayMetrics();
            lineWidthPx   = (int)(2 * dm.density);
            dotRadiusPx   = (int)(4 * dm.density);
            offset        = (int)(40 * dm.density); // 左侧留出 40dp 绘制区域

            linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            linePaint.setColor(0xFFCCCCCC);
            linePaint.setStrokeWidth(lineWidthPx);

            dotPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            dotPaint.setColor(0xFF3F51B5);
        }

        @Override
        public void onDrawOver(@NonNull Canvas canvas,
                               @NonNull RecyclerView parent,
                               @NonNull RecyclerView.State state) {
            int left   = parent.getPaddingLeft() + offset / 2;
            int right  = parent.getWidth() - parent.getPaddingRight();
            int childCount = parent.getChildCount();

            for (int i = 0; i < childCount; i++) {
                View child = parent.getChildAt(i);
                int pos    = parent.getChildAdapterPosition(child);
                // 计算圆心纵坐标
                float cx = left;
                float cy = (child.getTop() + child.getBottom()) / 2f;

                // 上半段线:若不是第0个,则绘制从上一个节点到当前节点的连线
                if (pos > 0) {
                    // 找到上一个节点的 cy
                    View prev = parent.getLayoutManager().findViewByPosition(pos - 1);
                    float prevCy = (prev != null)
                        ? (prev.getTop() + prev.getBottom()) / 2f
                        : child.getTop() - child.getHeight() / 2f;
                    canvas.drawLine(cx, prevCy, cx, cy - dotRadiusPx, linePaint);
                }
                // 下半段线:若不是最后一个,则绘制从当前节点到底下一个节点的连线
                if (pos < itemCount - 1) {
                    View next = parent.getLayoutManager().findViewByPosition(pos + 1);
                    float nextCy = (next != null)
                        ? (next.getTop() + next.getBottom()) / 2f
                        : child.getBottom() + child.getHeight() / 2f;
                    canvas.drawLine(cx, cy + dotRadiusPx, cx, nextCy, linePaint);
                }
                // 绘制节点圆点
                canvas.drawCircle(cx, cy, dotRadiusPx, dotPaint);
            }
        }
    }
}

4.2 XML 布局与资源

<!-- ===================================================================
     AndroidManifest.xml —— 应用入口声明
=================================================================== -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.timeline">
  <application
      android:allowBackup="true"
      android:label="Timeline Demo"
      android:icon="@mipmap/ic_launcher"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:theme="@style/Theme.AppCompat.Light.NoActionBar">
    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>
</manifest>

<!-- ===================================================================
     activity_main.xml —— 主界面布局:一个 RecyclerView
=================================================================== -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/recycler"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingVertical="16dp"/>

<!-- ===================================================================
     item_timeline.xml —— 列表项布局:左侧时间,右侧内容
=================================================================== -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:paddingVertical="12dp"
    android:paddingEnd="16dp">

    <!-- 时间文本-->
    <TextView
        android:id="@+id/tv_time"
        android:layout_width="80dp"
        android:layout_height="wrap_content"
        android:gravity="end"
        android:textColor="#666666"
        android:textSize="14sp"/>

    <!-- 右侧内容区-->
    <TextView
        android:id="@+id/tv_content"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:paddingStart="16dp"
        android:textColor="#333333"
        android:textSize="16sp"/>
</LinearLayout>

五、代码解读

  1. 数据模型 TimelineItem

    • 包含 time(时间字符串)和 content(事件描述),Adapter 根据它填充界面。

  2. TimelineAdapter

    • 单一 ViewType,通过 item_timeline.xml 显示左侧时间和右侧内容;

    • onBindViewHolder 简单填充文本。

  3. TimelineItemDecoration

    • onDrawOver() 中为每个可见 item 绘制时间轴:

      • 计算圆点的 X (left + offset)、Y(item 垂直中心)坐标;

      • 若不是首尾,分别从圆点上方/下方延伸线段至相邻节点位置;

      • 使用单例 Paint 对象提高性能;

      • offset 控制线段绘制的水平位置,与 item 左侧时间列对齐。

  4. MainActivity

    • 初始化数据并设置 RecyclerViewLinearLayoutManager + adapter + ItemDecoration

    • 数据变更后重新 notifyDataSetChanged() 即可自动重绘时间轴。


六、项目总结

  • 本文基于 RecyclerView.ItemDecoration 实现了一个简洁、高效的垂直时间轴控件。

  • 核心思路是:将“绘制时间轴”与“显示内容”分离,Adapter 只负责内容,Decoration 统一管理线条与节点的绘制。

  • 该方案无需额外第三方库,易于定制和扩展,适用于各种分步、日志、版本发布和社交时间线场景。


七、实践建议与未来展望

  1. 样式定制化

    • 将线条颜色、节点半径、时间文本宽度等抽象为自定义属性(attrs.xml),通过布局 XML 配置。

  2. 水平时间轴

    • 可参考同样思路在横向 RecyclerView 中实现水平时间轴。

  3. 动态交互

    • 节点点击支持展开更多详情,结合 RecyclerView 的点击监听或 ItemTouchHelper

  4. Jetpack Compose

    • 在 Compose 中可用 LazyColumn + Modifier.drawBehind{} 实现相同效果,且代码更加简洁。

  5. 动画

    • 为节点或线条添加入场动画(Alpha/Scale),提升视觉效果。


八、知识拓展与参考资料

  1. RecyclerView.ItemDecoration 文档

  2. Canvas API

  3. Android 自定义 View 属性

  4. 开源实现参考 “StickyTimeline”、“RecyclerViewTimelineDecoration” 等 GitHub 项目。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值