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

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

分析

实现这个最常用的一个方法就是用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); } }

效果图如下:

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

时间: 2024-09-13 03:33:58

Android自定义View实现垂直时间轴布局的相关文章

Android自定义View研究--View中的原点坐标和XML中布局自定义View时View触摸原点问题

这里只做个汇总~.~独一无二 文章出处:http://blog.csdn.net/djy1992/article/details/9715047 Android自定义View研究--View中的原点坐标相关问题 我们自定义了View,但是有没想过一个问题,就是View中的(0,0)坐标,也就是原点坐标在哪??我们是不是有时候很困惑,接下来我们就来研究View中的原点坐标相关的问题. 一.new DuView时View的原点 我们通过从View中绘制一条从原点到右下角的线来看看这个View中的原点

ontochevent事件冲突-Android 自定义布局和自定义view的onTochEvent时间冲突

问题描述 Android 自定义布局和自定义view的onTochEvent时间冲突 我自定义了一个布局和一个view,自定义view被布局引用.view和布局都重写了他的onTochEvent方法,然后发现布局的onTochEvent方法无法被执行被view的onTochEvent 拦截了,请问怎么解决. 解决方案 使自定义view来拦截touch事件即可 onInterceptTouchEvent 解决方案二: 刚好最近写了一篇关于View事件冲突的文章,你看看http://blog.csd

组合-android自定义view怎样指定自定义view的布局

问题描述 android自定义view怎样指定自定义view的布局 我有现成的布局xml文件,现在想定义一个组合的自定义view,怎样把这个view的布局指定为一个xml文件 解决方案 LayoutInflater.from(mActivity).inflate(R.layout.mainscreen_title, this, true);这样就行了,this是当前的View,而后面这两个参数是将R.layout.mainscreen_title attachToRoot 也就是以当前这个Vie

Android自定义View仿IOS圆盘时间选择器

通过自定义view实现仿iOS实现滑动两端的点选择时间的效果 效果图 自定义的view代码 public class Ring_Slide2 extends View { private static final double RADIAN = 180 / Math.PI; private int max_progress; // 设置最大进度 private int cur_progress; //设置锚点1当前进度 private int cur_progress2; //设置锚点2进度 p

Android自定义View之圆形进度条总结

最近撸了一个圆形进度条的开源项目,算是第一次完完整整的使用自定义 View .在此对项目开发思路做个小结,欢迎大家 Star 和 Fork. 该项目总共实现了三种圆形进度条效果 CircleProgress:圆形进度条,可以实现仿 QQ 健康计步器的效果,支持配置进度条背景色.宽度.起始角度,支持进度条渐变 DialProgress:类似 CircleProgress,但是支持刻度 WaveProgress:实现了水波纹效果的圆形进度条,不支持渐变和起始角度配置,如需此功能可参考 CircleP

Android自定义view实现阻尼效果的加载动画_Android

效果: 需要知识: 1. 二次贝塞尔曲线 2. 动画知识 3. 基础自定义view知识 先来解释下什么叫阻尼运动 阻尼振动是指,由于振动系统受到摩擦和介质阻力或其他能耗而使振幅随时间逐渐衰减的振动,又称减幅振动.衰减振动.[1] 不论是弹簧振子还是单摆由于外界的摩擦和介质阻力总是存在,在振动过程中要不断克服外界阻力做功,消耗能量,振幅就会逐渐减小,经过一段时间,振动就会完全停下来.这种振幅随时间减小的振动称为阻尼振动.因为振幅与振动的能量有关,阻尼振动也就是能量不断减少的振动.阻尼振动是非简谐运

Android 自定义View步骤_Android

例子如下:Android 自定义View 密码框 例子 1 良好的自定义View 易用,标准,开放. 一个设计良好的自定义view和其他设计良好的类很像.封装了某个具有易用性接口的功能组合,这些功能能够有效地使用CPU和内存,并且十分开放的.但是,除了开始一个设计良好的类之外,一个自定义view应该: l 符合安卓标准 l 提供能够在Android XML布局中工作的自定义样式属性 l 发送可访问的事件 l 与多个Android平台兼容. Android框架提供了一套基本的类和XML标签来帮您创

Android自定义View实现照片裁剪框与照片裁剪功能_Android

本文所需要实现的就是这样一种有逼格的效果: 右上角加了个图片框,按下确定可以裁剪正方形区域里的图片并显示在右上角. 实现思路: 1:首先需要自定义一个ZoomImageView来显示我们需要的图片,这个View需要让图片能够以合适的位置展现在当前布局的图片展示区域内(合适的位置值的是:如果图片长度大于屏幕,则压缩图片长度至屏幕宽度,高度等比压缩并居中显示,如果图片高度大于屏幕,则压缩图片高度至屏幕高度,长度等比压缩并居中显示.): 2:然后需要实现这个拖动的框框,该框框实现的功能有四点:拖动.扩

Android自定义View之酷炫圆环(二)_Android

先看下最终的效果 静态: 动态: 一.开始实现 新建一个DoughnutProgress继承View public class DoughnutProgress extends View { } 先给出一些常量.变量以及公共方法的代码,方便理解后面的代码     private static final int DEFAULT_MIN_WIDTH = 400; //View默认最小宽度 private static final int RED = 230, GREEN = 85, BLUE =