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

效果图如下:

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

(0)

相关推荐

  • Android控件之使用ListView实现时间轴效果

     实现思路: 该View是通过ListView实现的,通过实体两个字段内容content和时间time来展示每个ListItem 时间轴是使用上面一条线(20dp)和中间一个圆(15dp)和下面一条线(40dp)组装成的 在ListView中,设置其分割线为空,并且没有点击效果 效果图: 步骤一:使用xml画出一个灰色的圆点(time_cycle.xml) <?xml version="1.0" encoding="utf-8"?> <shape

  • Android自定义时间轴的实现过程

    本文讲述Android自定义时间轴的实现过程,供大家参考,具体内容如下 相关视频链接: Android自定义控件系列 http://edu.csdn.net/course/detail/3719/65396 Android视频全系列 http://edu.csdn.net/course/detail/2741/43163 时间轴效果,实际上非常简单,就是listView中一个又一个的条目而已-.大家可以只关注一个条目. 首先展示一个条目的布局效果 <?xml version="1.0&qu

  • 教你3分钟了解Android 简易时间轴的实现方法

    一.有段时间没更了,因为一直在思索,应该写点什么,真的是无比纠结.这一回,就给大家分享一款简便好用的,小编自制的土晾时间轴. 附上XML预览图: 效果图 注:小黄鸭不是效果哈,是为了保护个人隐私P上去的: 1.新建一个自定义控件: public class WorkExcView extends LinearLayout { private TextView dataLeft; private TextView dataRight; private TextView company; priva

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

    时间轴,顾名思义就是将发生的事件按照时间顺序罗列起来,给用户带来一种更加直观的体验.京东和淘宝的物流顺序就是一个时间轴,想必大家都不陌生,如下图: 分析 实现这个最常用的一个方法就是用ListView,我这里用继承LinearLayout的方式来实现.首先定义了一些自定义属性: attrs.xml <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable

  • Android使用自定义View实现横行时间轴效果

    前言 本篇文章会说下如何使用并且要用麻烦的自定义 view 去实现时间轴效果,以及如何分析.实现自定义 view. 需要具备的知识:Paint.Canvas.自定义 view 的绘制流程. 欢迎留言交流. 一.已经有很多 RecycleView 实现时间轴的例子,为何还要费劲的使用自定义 view 去实现时间轴? 首先看下最终想要的效果: 根据上图可以总结出以下几点: 每个阶段要显示时间.阶段名.状态图标.中间有虚线: 文字上下交错显示: 相邻阶段的文字在垂直方向上是可以相交的: 时间轴的个数不

  • Android自定义View实现比赛时间闪动效果

    本文实例为大家分享了Android实现比赛时间闪动效果的具体代码,供大家参考,具体内容如下 效果 代码 上代码 public class TwinkleTextView extends TextView implements Runnable { /** * 是否显示 */ private boolean flag = true; /** * 闪动内容 */ private String twinkleText = "'"; /** * 闪动时间 */ private int dela

  • Android自定义View实现仿1号店垂直滚动广告条代码

    效果图展示,图片有点卡,耐心看会,原程序是很流畅的 实现步骤: 声明变量 初始化画笔.文本大小和坐标 onMeasure()适配wrap_content的宽高 onDraw()画出根据坐标画出两段Text 监听点击事件 在Activity中实现点击事件 实现原理(坐标变换原理):整个过程都是基于坐标Y的增加和交换进行处理的,Y值都会一直增加到endY,然后进行交换逻辑 步骤一:声明变量 由于1号店是两句话的滚动,所以我们也是使用两句话来实现的 private Paint mPaint; priv

  • 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实现任意布局的RadioGroup效果

    前言 RadioGroup是继承LinearLayout,只支持横向或者竖向两种布局.所以在某些情况,比如多行多列布局,RadioGroup就并不适用 . 本篇文章通过继承RelativeLayout实现自定义RadioGroup,实现RadioButton的任意布局.效果图如下: 代码(RelativeRadioGroup) /** * Author : BlackHao * Time : 2018/10/26 10:46 * Description : 自定义 RadioGroup */ p

  • Android自定义view之围棋动画效果的实现

    前言 废话不多说直接开始 老规矩,文章最后有源码 完成效果图 棋子加渐变色 棋子不加渐变色 一.测量 1.获取宽高 @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = w; mHeight = h; useWidth = mWidth; if (mWidth > mHeight) { useWidth =

  • Android recyclerview实现纵向虚线时间轴的示例代码

    效果图 代码 package com.jh.timelinedemo; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.DashPathEffect; import android.graphics.Paint; import android.util.AttributeSet; import

  • Android自定义View实现APP启动页倒计时效果

    Android自定义View实现APP启动页倒计时效果,供大家参考,具体内容如下 之前也是做过APP启动页的倒计时效果,但是只有文字变化,没有动画效果,这次通过使用自定义View控件来制作一个带有动画效果的倒计时. 倒计时效果的基本思路如下: Canvas提供了绘制弧形的方法,drawArc(),使用这个方法通过定时刷新计算当前弧形的角度,就可以模拟出倒计时的动画效果,同时借助drawText()方法可以实现倒计时文字.(1)继承View(2)使用canvas的drawArc()来绘制弧形,模拟

  • Android自定义View实现绘制水波浪温度刻度表

    目录 前言 1.onMeasure重新测量 2.绘制刻度 3. 设置刻度动画 4. 绘制中心的圆与文字 5. 水波纹动画 后记 前言 之前的绘制圆环,我们了解了如何绘制想要的形状和进度的一些特点,那么此篇文章我们更近一步,绘制一个稍微复杂一点的刻度与波浪.来一起复习一下Android的绘制. 相对应的这种类型的自定义View网上并不少见,但是如果我们要做一些个性化的效果,最好还是自己绘制一份,也相对的比较容易控制效果,如果想实现上面的效果,我们一般来说分为以下几个步骤: 重写测量方法,确保它是一

随机推荐