时间轴
时间轴,顾名思义就是将发生的事件按照时间顺序罗列起来,给用户带来一种更加直观的体验。京东和淘宝的物流顺序就是一个时间轴(如图),想必大家都不陌生。
时间轴的初探
初次见到这种UI,感觉整个布局似曾相识,但面对那条时间轴却又不知如何下手。其实,整个时间轴还是可以当做一个ListView处理,只是在Adapter中的布局要花点心思。好了,废话不说,看代码。
Attrs 属性
开始控件之前先准备好需要的属性。
<resources>
<declare-styleable name="TimeLineMarker">
<attr name="markerSize" format="dimension" />
<attr name="marker" format="color|reference" />
<attr name="beginLine" format="color|reference" />
<attr name="endLine" format="color|reference" />
<attr name="lineSize" format="dimension" />
</declare-styleable>
</resources>
控件很简单,首先我们继承View,取名为 TimeLineMarker 就OK。
public class TimeLineMarker extends View {
private int mMarkerSize = 24;
private int mLineSize = 12;
private Drawable mBeginLine;
private Drawable mEndLine;
private Drawable mMarkerDrawable;
@Override
protected void onDraw(Canvas canvas) {
if (mBeginLine != null) {
mBeginLine.draw(canvas);
}
if (mEndLine != null) {
mEndLine.draw(canvas);
}
if (mMarkerDrawable != null) {
mMarkerDrawable.draw(canvas);
}
super.onDraw(canvas);
}
public TimeLineMarker(Context context) {
this(context, null);
}
public TimeLineMarker(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TimeLineMarker(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs);
}
private void init(AttributeSet attrs) {
// Load attributes
final TypedArray a = getContext().obtainStyledAttributes(
attrs, R.styleable.TimeLineMarker);
mMarkerSize = a.getDimensionPixelSize(
R.styleable.TimeLineMarker_markerSize,
mMarkerSize);
mLineSize = a.getDimensionPixelSize(
R.styleable.TimeLineMarker_lineSize,
mLineSize);
mBeginLine = a.getDrawable(
R.styleable.TimeLineMarker_beginLine);
mEndLine = a.getDrawable(
R.styleable.TimeLineMarker_endLine);
mMarkerDrawable = a.getDrawable(
R.styleable.TimeLineMarker_marker);
a.recycle();
if (mBeginLine != null)
mBeginLine.setCallback(this);
if (mEndLine != null)
mEndLine.setCallback(this);
if (mMarkerDrawable != null)
mMarkerDrawable.setCallback(this);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int w = getPaddingLeft() + getPaddingRight();
int h = getPaddingTop() + getPaddingBottom();
if (mMarkerDrawable != null) {
w += mMarkerSize;
h += mMarkerSize;
}
w = Math.max(w, getMeasuredWidth());
h = Math.max(h, getMeasuredHeight());
int widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);
int heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
initDrawableSize();
}
private void initDrawableSize() {
int pLeft = getPaddingLeft();
int pRight = getPaddingRight();
int pTop = getPaddingTop();
int pBottom = getPaddingBottom();
int width = getWidth();
int height = getHeight();
int cWidth = width - pLeft - pRight;
int cHeight = height - pTop - pBottom;
Rect bounds;
if (mMarkerDrawable != null) {
// Size
int markerSize = Math.min(mMarkerSize, Math.min(cWidth, cHeight));
mMarkerDrawable.setBounds(pLeft, pTop,
pLeft + markerSize, pTop + markerSize);
bounds = mMarkerDrawable.getBounds();
} else {
bounds = new Rect(pLeft, pTop, pLeft + cWidth, pTop + cHeight);
}
int halfLineSize = mLineSize >> 1;
int lineLeft = bounds.centerX() - halfLineSize;
if (mBeginLine != null) {
mBeginLine.setBounds(lineLeft, 0, lineLeft + mLineSize, bounds.top);
}
if (mEndLine != null) {
mEndLine.setBounds(lineLeft, bounds.bottom, lineLeft + mLineSize, height);
}
}
public void setLineSize(int lineSize) {
if (mLineSize != lineSize) {
this.mLineSize = lineSize;
initDrawableSize();
invalidate();
}
}
public void setMarkerSize(int markerSize) {
if (this.mMarkerSize != markerSize) {
mMarkerSize = markerSize;
initDrawableSize();
invalidate();
}
}
public void setBeginLine(Drawable beginLine) {
if (this.mBeginLine != beginLine) {
this.mBeginLine = beginLine;
if (mBeginLine != null) {
mBeginLine.setCallback(this);
}
initDrawableSize();
invalidate();
}
}
public void setEndLine(Drawable endLine) {
if (this.mEndLine != endLine) {
this.mEndLine = endLine;
if (mEndLine != null) {
mEndLine.setCallback(this);
}
initDrawableSize();
invalidate();
}
}
public void setMarkerDrawable(Drawable markerDrawable) {
if (this.mMarkerDrawable != markerDrawable) {
this.mMarkerDrawable = markerDrawable;
if (mMarkerDrawable != null) {
mMarkerDrawable.setCallback(this);
}
initDrawableSize();
invalidate();
}
}
}
使用
XML布局
ITEM布局item_time_line.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.example.v_xinxxliu.recycleview_video_demo.TimeLineMarker
android:id="@+id/item_time_line_mark"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingBottom="16dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingTop="16dp"
app:lineSize="2dp"
app:beginLine="@color/colorAccent"
app:endLine="@color/colorAccent"
app:marker="@drawable/pause"
app:markerSize="24dp"/>
<TextView
android:id="@+id/item_time_line_txt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingBottom="16dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingTop="16dp"
android:textColor="@color/colorPrimary"
android:textSize="16dp" />
</LinearLayout>
在这里我们之间使用顺序布局,左边是TimelIne控件,右边是一个简单的字体控件,具体使用中可以细化一些。
在TImeLine控件中我们的Mark是使用的drawable/ic_timeline_default_marker;这个就是一个简单的圆圈而已;对于自己美化可以使用一张图片代替或者更加复杂的布局;当然上面的线条就更加简单了,就直接使用颜色代替。
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/colorAccent" />
<stroke
android:width="1dp"
android:color="@color/colorPrimary" />
</shape>
主界面XML RecyclerView
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/time_line_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
数据类
public class TimeLineModel {
private String name;
private int age;
public TimeLineModel() {
}
public TimeLineModel(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
}
ItemType.java
public class ItemType {
public final static int NORMAL = 0;
public final static int HEADER = 1;
public final static int FOOTER = 2;
public final static int START = 4;
public final static int END = 8;
public final static int ATOM = 16;
}
TimeLineViewHolder.java
public class TimeLineViewHolder extends RecyclerView.ViewHolder {
private TextView mName;
public TimeLineViewHolder(View itemView, int type) {
super(itemView);
mName = (TextView) itemView.findViewById(R.id.item_time_line_txt);
TimeLineMarker mMarker = (TimeLineMarker) itemView.findViewById(R.id.item_time_line_mark);
if (type == ItemType.ATOM) {
mMarker.setBeginLine(null);
mMarker.setEndLine(null);
} else if (type == ItemType.START) {
mMarker.setBeginLine(null);
} else if (type == ItemType.END) {
mMarker.setEndLine(null);
}
}
public void setData(TimeLineModel data) {
mName.setText("Name:" + data.getName() + " Age:" + data.getAge());
}
}
该文件为RecyclerView 的Adapter中每个Item需要实现的Holder类。
在该类中,我们在构造函数中需要传入一个根View同时传入一个当然item的状态。
随后使用find….找到控件,在这里我们把TextView保存起来,而TimeLineView找到后直接进行初始化设置。
根据传入的ItemType来判断是否是第一个,最后一个,以及原子;然后设置TimeLineView的属性。
在下面的setData方法中我们显示具体的Model数据。
TimeLineAdapter.java
适配器部分,我们需要做的工作是;根据具体的数据渲染上对应的界面就OK。
public class TimeLineAdapter extends RecyclerView.Adapter<TimeLineViewHolder> {
private List<TimeLineModel> mDataSet;
public TimeLineAdapter(List<TimeLineModel> models) {
mDataSet = models;
}
@Override
public int getItemViewType(int position) {
final int size = mDataSet.size() - 1;
if (size == 0)
return ItemType.ATOM;
else if (position == 0)
return ItemType.START;
else if (position == size)
return ItemType.END;
else return ItemType.NORMAL;
}
@Override
public TimeLineViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
// Create a new view.
View v = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_time_line, viewGroup, false);
return new TimeLineViewHolder(v, viewType);
}
@Override
public void onBindViewHolder(TimeLineViewHolder timeLineViewHolder, int i) {
timeLineViewHolder.setData(mDataSet.get(i));
}
@Override
public int getItemCount() {
return mDataSet.size();
}
}
在这里需要着重说一下:我复写了getItemViewType方法;在该方法中我们需要设置对应的Item的类型;在这里传入的是item的坐标,需要返回的是item的具体状态,该状态标示是int类型;在这里我使用的是ItemType的静态属性。
该方法会在调用onCreateViewHolder方法之前调用;而onCreateViewHolder方法中的第二个参数int值也就是从getItemViewType之中来;所以我们可以在这里进行对应的数据状态标示。
而在onCreateViewHolder方法中我们返回一个:TimeLineViewHolder就OK,随后在onBindViewHolder方法中进行数据初始化操作。
MainActivity.java
上面所有都准备好了,下面就进行具体的显示。
在这里就只贴出核心代码了;篇幅也是有些长。
public class MainActivity extends AppCompatActivity {
private RecyclerView mRecycler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecycler = (RecyclerView) findViewById(R.id.time_line_recycler);
initRecycler();
}
private void initRecycler() {
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
TimeLineAdapter adapter = new TimeLineAdapter(getData());
mRecycler.setLayoutManager(layoutManager);
mRecycler.setAdapter(adapter);
}
private List<TimeLineModel> getData() {
List<TimeLineModel> models = new ArrayList<TimeLineModel>();
models.add(new TimeLineModel("XiaoMing", 21));
models.add(new TimeLineModel("XiaoFang", 20));
models.add(new TimeLineModel("XiaoHua", 25));
models.add(new TimeLineModel("XiaoA", 22));
models.add(new TimeLineModel("XiaoNiu", 23));
return models;
}
}
大功告成!!!!