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

少年乘勇气,百战过乌孙。这篇文章主要讲述Android自定义View实现垂直时间轴布局相关的知识,希望能为你提供帮助。
时间轴
时间轴, 顾名思义就是将发生的事件按照时间顺序罗列起来, 给用户带来一种更加直观的体验。京东和淘宝的物流顺序就是一个时间轴, 想必大家都不陌生, 如下图:

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); } }

效果图如下:
Android自定义View实现垂直时间轴布局

文章图片
Android自定义View实现垂直时间轴布局

文章图片










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


    推荐阅读