RecyclerView 列表控制元件中簡單實現時間線

Qiujuer發表於2015-08-24

時間

時間,時間,時間啊;走慢一點吧~

看見很多軟體中都有時間線的東西,貌似天氣啊,旅遊啊什麼的最多了;具體實現方式很多,在本篇文章中講解一種自定義View封裝的方式。

效果

先來看看效果。
這裡寫圖片描述

分析

軟體中,可以看見前面的時間線也就是線條加上圓圈組成;當然這裡的圓圈與線條也都是可以隨意換成其他的,比如圖片等等。

當然這裡最簡單的來說,是上面一個線條,然後一個圓圈,然後下面一個線條;上線條在第一條資料時不做顯示,下線條在最後一條資料時不做顯示。

這裡寫圖片描述

這裡自定義佈區域性分也就是把旁邊的線條與圓圈封裝到一起,並使用簡單的方法來控制是否顯示。
當封裝好了後,與旁邊的文字部分也就是水瓶方向的線性佈局了,然後設定為每一個的RecyclerView 的Item的佈局也就完成了。

控制元件

控制元件很簡單,首先我們繼承View,取名為 TimeLineMarker 就OK。

Attrs 屬性

開始控制元件之前先準備好需要的屬性。

<?xml version="1.0" encoding="utf-8"?>
<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>

在這裡也就準備了線條的大小、開始線條、結束線條、中間標示部分及大小。

屬性與現實

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

兩個大小屬性,3個具體的Drawable,然後在onDraw方法中進行具體的顯示也就OK。

構造與屬性初始化

在上面我們定義了屬性,在這裡我們在建構函式中獲取XML所設定的屬性。


    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, 0, 0);

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

Drawable 的位置與大小初始化

屬性啥的有了,具體的Drawable 也有了,要顯示的地方呼叫也是OK了;但是如果沒有進行進行具體的位置調整這一切也都沒有意義。

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

initDrawableSize 方法進行具體的運算,而運算的時間點就是當控制元件的大小改變(onSizeChanged)的時候。

在初始化中採用了一定的投機取巧;這裡利用了上內邊距與下內邊距分別作為上線條與下線條的長度;而線條與中間的標識都採用了水平距中。

其他設定方法


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

在設定中,首先判斷是否更改,如果更改那麼就更新並重新計算位置;隨後重新整理介面。

到這裡,控制元件差不多準備OK了,其中還有很多可以完善的地方,比如加上快捷設定顏色什麼的,也可以加上大小計算的東西。同時還可以加上時間線是水瓶還是垂直等等。在這裡就不累贅介紹哪些了。下面來看看如何使用。

使用

XML佈局

ITEM佈局item_time_line.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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:paddingLeft="@dimen/lay_16"
    android:paddingRight="@dimen/lay_16"
    tools:ignore="MissingPrefix">

    <net.qiujuer.example.timeline.widget.TimeLineMarker
        android:id="@+id/item_time_line_mark"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/lay_16"
        android:paddingLeft="@dimen/lay_4"
        android:paddingRight="@dimen/lay_4"
        android:paddingTop="@dimen/lay_16"
        app:beginLine="@color/black_alpha_32"
        app:endLine="@color/black_alpha_32"
        app:lineSize="2dp"
        app:marker="@drawable/ic_timeline_default_marker"
        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="@dimen/lay_16"
        android:paddingLeft="@dimen/lay_4"
        android:paddingRight="@dimen/lay_4"
        android:paddingTop="@dimen/lay_16"
        android:textColor="@color/grey_600"
        android:textSize="@dimen/font_16" />

</LinearLayout>

在這裡我們之間使用順序佈局,左邊是TimelIne控制元件,右邊是一個簡單的字型控制元件,具體使用中可以細化一些。
在TImeLine控制元件中我們的Mark是使用的drawable/ic_timeline_default_marker;這個就是一個簡單的圓圈而已;對於自己美化可以使用一張圖片代替或者更加複雜的佈局;當然上面的線條就更加簡單了,就直接使用顏色代替。

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid android:color="@color/cyan_500" />
    <stroke
        android:width="1dp"
        android:color="@color/black_alpha_32" />
</shape>

主介面XML RecyclerView

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/time_line_recycler"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clickable="true"
        android:fadeScrollbars="true"
        android:fadingEdge="none"
        android:focusable="true"
        android:focusableInTouchMode="true"
        android:overScrollMode="never"
        android:scrollbarSize="2dp"
        android:scrollbarThumbVertical="@color/cyan_500"
        android:scrollbars="vertical" />

</RelativeLayout>

在這裡就是加上了一個RecyclerView 控制元件在主介面就OK。

Java程式碼部分

在開始之前先來看看我們的檔案具體有些神馬。
這裡寫圖片描述

widget中就是具體的自定義控制元件,model是具體的資料模型,adapter部分,這裡有一個Recyclerview的adapter檔案,以及一個具體的Item TimeLineViewHolder,當然在這裡還定義了一個ItemType類,該類用來標示每個Item的型別,比如頭部,第一個,普通,最後一個,底部等等。

TimeLineModel.java

package net.qiujuer.example.timeline.model;

/**
 * Created by qiujuer
 * on 15/8/23.
 */
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;
    }
}

一個名字,一個年齡也就OK。

ItemType.java

package net.qiujuer.example.timeline.adapter;

/**
 * Created by qiujuer
 * on 15/8/23.
 */
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

package net.qiujuer.example.timeline.adapter;

import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.TextView;

import net.qiujuer.example.timeline.R;
import net.qiujuer.example.timeline.model.TimeLineModel;
import net.qiujuer.example.timeline.widget.TimeLineMarker;

/**
 * Created by qiujuer
 * on 15/8/23.
 */
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。

package net.qiujuer.example.timeline.adapter;

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import net.qiujuer.example.timeline.R;
import net.qiujuer.example.timeline.model.TimeLineModel;

import java.util.List;

/**
 * Created by qiujuer
 * on 15/8/23.
 */
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

上面所有都準備好了,下面就進行具體的顯示。
在這裡就只貼出核心程式碼了;篇幅也是有些長。


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

在這裡就是傻瓜的操作了,流程就是準備好對應的資料,裝進Adapter,準備好對應的佈局方式,然後都設定到RecyclerView中就OK。

效果

來看看具體的效果:
這裡寫圖片描述

效果雖然簡單,但是也算是五臟具全;其中無非就是控制元件的自定義。這個自定義是可以擴充套件的,大家可以擴充套件為水平方向試試。

程式碼

部落格程式碼都開源到專案中了。
地址:https://github.com/qiujuer/BeFoot/tree/master/blog/sample/TimeLine

寫在最後

文章的開始截圖來源於:最近沒事兒搗鼓了一個APP[UPMiss],一個簡單的生日,紀念日提醒軟體;歡迎大家嚐鮮。

UPMiss} 思念你的夏天
下載地址:

  • 魅族
  • 百度 這個稽核有問題,明明沒有支付的東西,結果說有支付的SDK存在,不得不說百度的自動稽核有很大漏洞。
  • 豌豆莢 新版2.0還在稽核中!

========================================================
作者:qiujuer
部落格:blog.csdn.net/qiujuer
網站:www.qiujuer.net
開源庫:github.com/qiujuer/Genius-Android
開源庫:github.com/qiujuer/Blink
轉載請註明出處:http://blog.csdn.net/qiujuer/article/details/47910185
—— 學之開源,用於開源;初學者的心態,與君共勉!

========================================================

相關文章