Android 自定義 View 實(shí)現(xiàn)橫行時(shí)間軸

篇文章會(huì)說(shuō)下如何使用并且要用麻煩的自定義 view 去實(shí)現(xiàn)時(shí)間軸效果移剪,以及如何分析简识、實(shí)現(xiàn)自定義 view荆萤。 需要具備的知識(shí):Paint、Canvas矩父、自定義 view 的繪制流程锉桑。

一、已經(jīng)有很多 RecycleView 實(shí)現(xiàn)時(shí)間軸的例子窍株,為何還要費(fèi)勁的使用自定義 view 去實(shí)現(xiàn)時(shí)間軸民轴?

首先看下最終想要的效果:

根據(jù)上圖可以總結(jié)出以下幾點(diǎn):

  1. 每個(gè)階段要顯示時(shí)間、階段名球订、狀態(tài)圖標(biāo)后裸、中間有虛線;
  2. 文字上下交錯(cuò)顯示冒滩;
  3. 相鄰階段的文字在垂直方向上是可以相交的微驶;
  4. 時(shí)間軸的個(gè)數(shù)不確定,但是要鋪滿屏幕并且不可滑動(dòng)开睡; 如果只實(shí)現(xiàn)上兩點(diǎn)的效果因苹,使用 RecycleView 無(wú)疑是最好的選擇,但是要同時(shí)實(shí)現(xiàn)以上整個(gè)效果目前想到的最好的辦法就是使用自定義 view篇恒。

二扶檐、如何開(kāi)始?

相信也有人跟我一樣婚度,對(duì)自定義的繪制過(guò)程 view蘸秘、canvas官卡、path、paint 的使用有了解醋虏,但是真的要去寫(xiě)自定義 view 確不知道從何開(kāi)始寻咒,不知道第一步如何下手。我個(gè)人的總結(jié)就是:想要的太多颈嚼,遲遲不動(dòng)手毛秘,所以有想法一定要去動(dòng)手試驗(yàn)! 不要想著寫(xiě)完第一次運(yùn)行就是最終想展示的完美效果阻课,而是要抱著整體拆分成不重復(fù)的小塊叫挟,然后去繪制重復(fù)塊,然后去一點(diǎn)點(diǎn)實(shí)現(xiàn)一步步完美的心態(tài)才能做出來(lái)限煞。

所以首先要把想實(shí)現(xiàn)的 view 拆分成一個(gè)個(gè)小的可繪制的并且沒(méi)有重復(fù)的塊抹恳,以目前想實(shí)現(xiàn)的時(shí)間軸效果來(lái)說(shuō),最小可繪制無(wú)重復(fù)塊也就是只包括一個(gè)時(shí)間結(jié)點(diǎn)的塊如圖:

它包括:

  • 垂直居中的一條虛線署驻;
  • 一個(gè)表示狀態(tài)的圖標(biāo)奋献;
  • 一個(gè)顯示時(shí)間的文本塊;
  • 一個(gè)顯示階段名的文本塊旺上;

三瓶蚂、開(kāi)始畫(huà)

有了上面的分析,接下來(lái)就要開(kāi)始畫(huà)了宣吱。

1. 畫(huà)中間的線

首先畫(huà)虛線窃这,如果虛線不知道怎么畫(huà),可以先畫(huà)一條實(shí)線征候,然后再去找畫(huà)虛線的方法杭攻。

使用 canvas 中畫(huà)線的方法 drawLine(float startX, float startY, float stopX, float stopY, @NonNull Paint paint),根據(jù)參數(shù)得知需知道線的起點(diǎn)與終點(diǎn)坐標(biāo)以及一個(gè) paint 對(duì)象疤坝,因?yàn)槭谴怪本又星覚M穿整個(gè)控件的直線所以可以確定兩個(gè)點(diǎn)的 y 坐標(biāo)是一樣的朴上,也就是控件高的一半,起點(diǎn)的 x 坐標(biāo)為0卒煞,終點(diǎn)的 x 坐標(biāo)為控件的寬。也就是知道控件的寬和高之后就可以繪制出這條線叼架。獲取控件的寬高畔裕,可以在 onMeasure 方法中獲取:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    mViewWidth = MeasureSpec.getSize(widthMeasureSpec) - dip2px(mContext, mSafeDistance * 2);
    mViewHeight = MeasureSpec.getSize(heightMeasureSpec);
}

畫(huà)線的代碼(在 onDraw 方法中添加乖订,下面其它的繪制方法同樣是在 onDraw 方法中添加):

// 定義畫(huà)筆扮饶,并設(shè)置相關(guān)屬性
Paint mLinePaint = new Paint();
mLinePaint.setColor(Color.parseColor("#999999"));
mLinePaint.setStrokeWidth(1);
mLinePaint.setStyle(Paint.Style.STROKE);
// 畫(huà)虛線
canvas.drawLine(0, mViewHeight / 2, mViewWidth, mViewHeight / 2, mLinePaint);

2. 畫(huà)圖標(biāo)

canvas 畫(huà)圖標(biāo)的方法:drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint),根據(jù)方法的參數(shù)去分析如何準(zhǔn)備值乍构,這里需要一個(gè) bitmap 對(duì)象甜无,起點(diǎn)坐標(biāo)以及 paint。bitmap 對(duì)象可以將資源文件 drawable 轉(zhuǎn)為 bitmap 格式;坐標(biāo)就是控件的中心點(diǎn)岂丘。畫(huà)圖標(biāo)的代碼:

// 圖標(biāo) x陵究,y 坐標(biāo)
Bitmap statusBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.ic_no_pass);
float bX = mViewWidth / 2;
// 垂直的中心點(diǎn)在圖標(biāo)的頂部,所以要減去 bitmap 高的一半
float bY = mViewHeight / 2 - statusBitmap.getHeight() / 2f;
Paint mBitmapPaint = new Paint();
mBitmapPaint.setFilterBitmap(true);
canvas.drawBitmap(statusBitmap, bX, bY, mBitmapPaint);

3. 畫(huà)文本

canvas 畫(huà)文本的方法:drawText(@NonNull String text, float x, float y, @NonNull Paint paint)奥帘,依然是根據(jù)方法得知需要知道繪制的內(nèi)容铜邮,開(kāi)始的坐標(biāo)點(diǎn)以及 paint。當(dāng)文本在圖標(biāo)上方時(shí)寨蹋,文本的 y 坐標(biāo)需要使用圖標(biāo)的 y 坐標(biāo)減去文本到圖標(biāo)的距離松蒜,x 坐標(biāo)同圖片的 x 坐標(biāo)一樣;當(dāng)文本在圖標(biāo)下方時(shí)已旧,文本的 y 坐標(biāo)需要使用圖標(biāo)的 y 坐標(biāo)加上文本到圖標(biāo)的距離秸苗。畫(huà)文本的代碼:

// 定義畫(huà)筆
Paint mDatePaint = new Paint();
mDatePaint.setColor(Color.parseColor("#666666"));
mDatePaint.setTextSize(dip2px(mContext, 12));
mDatePaint.setStyle(Paint.Style.FILL);
mDatePaint.setTextAlign(Paint.Align.CENTER);
mDatePaint.setAntiAlias(true);

Paint mNamePain = new Paint();
mNamePain.setColor(Color.parseColor("#666666"));
mNamePain.setTextSize(dip2px(mContext, 13));
mNamePain.setStyle(Paint.Style.FILL);
mNamePain.setTextAlign(Paint.Align.CENTER);
mNamePain.setAntiAlias(true);

// 定義坐標(biāo)變量
float dateX = bX + statusBitmap.getWidth() / 2f;
float dateY;
dateY = mViewHeight / 2 - dip2px(mContext, 19);

// 畫(huà)文字,在圖標(biāo)上
canvas.drawText("有效時(shí)間", dateX, dateY, mNamePain);
canvas.drawText("09.27-09.29", dateX, dateY - dateTextHeight mDatePaint);

// 畫(huà)文字运褪,在圖標(biāo)下
dateY = mViewHeight / 2 + dip2px(mContext, 19);
canvas.drawText("09.27-09.29", dateX, dateY, mDatePaint);
canvas.drawText("有效時(shí)間", dateX, dateY + dateTextHeigh, mNamePain);

4. 由局部到整體

上面已經(jīng)完成了只有一個(gè)時(shí)間點(diǎn)的繪制惊楼,接下來(lái)思考如果有多個(gè)時(shí)間點(diǎn)時(shí)如何繪制。只有一個(gè)時(shí)間點(diǎn)時(shí)計(jì)算坐標(biāo)是以控件的寬高進(jìn)行計(jì)算吐句,那么當(dāng)有兩個(gè)時(shí)間點(diǎn)的時(shí)候需要首先把控件均分成兩部分胁后,然后在均分的部分中計(jì)算對(duì)應(yīng)的坐標(biāo),完成繪制嗦枢。當(dāng)有三個(gè)時(shí)間點(diǎn)的時(shí)候需要均分為三部分攀芯,然后在各自的部分計(jì)算對(duì)應(yīng)的坐標(biāo),完成繪制文虏。所以得到不論時(shí)間點(diǎn)的個(gè)數(shù)有多少繪制的方法不會(huì)改變侣诺,需要改變的是繪制時(shí)候用到的點(diǎn)的坐標(biāo)。其實(shí)已經(jīng)可以看出氧秘,當(dāng)多個(gè)點(diǎn)的時(shí)候需要循環(huán)一下年鸳,代碼如下:

// 得到多個(gè)點(diǎn)時(shí),其中每個(gè)部分的寬丸相,itevW 也就等同與上面只有一個(gè)時(shí)間點(diǎn)時(shí)控件的寬
float itemW = mViewWidth / mDataList.size();
for (int i = 0; i < mDataList.size(); i++) {
    // 完成相關(guān)計(jì)算搔确、繪制
}

5. 完善

到這里,整個(gè)分析及繪制就差不多要結(jié)束了灭忠。

繪制虛線的方法:mLinePaint.setPathEffect(new DashPathEffect(new float[]{10, 10}, 0));

繪制虛線時(shí)遇到一個(gè)問(wèn)題膳算,在手機(jī)上不顯示虛線效果,后來(lái)查到需要關(guān)閉 view 層的硬件加速:setLayerType(View.LAYER_TYPE_SOFTWARE, null);

繪制文本時(shí)的中心點(diǎn)計(jì)算需要注意下弛作,可以 參考

paint涕蜂、bitmap 等變量的初始化建議放到初始化方法中去做,不建議在 onDraw 方法中做映琳。

四机隙、總結(jié)

關(guān)于自定義 view 一定要多看蜘拉,多嘗試。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末有鹿,一起剝皮案震驚了整個(gè)濱河市旭旭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌印颤,老刑警劉巖您机,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異年局,居然都是意外死亡际看,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)矢否,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)仲闽,“玉大人,你說(shuō)我怎么就攤上這事僵朗±敌溃” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵验庙,是天一觀的道長(zhǎng)顶吮。 經(jīng)常有香客問(wèn)我,道長(zhǎng)粪薛,這世上最難降的妖魔是什么悴了? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮违寿,結(jié)果婚禮上湃交,老公的妹妹穿的比我還像新娘。我一直安慰自己藤巢,他們只是感情好搞莺,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著掂咒,像睡著了一般才沧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上绍刮,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天糜工,我揣著相機(jī)與錄音,去河邊找鬼录淡。 笑死,一個(gè)胖子當(dāng)著我的面吹牛油坝,可吹牛的內(nèi)容都是我干的嫉戚。 我是一名探鬼主播刨裆,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼彬檀!你這毒婦竟也來(lái)了帆啃?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤窍帝,失蹤者是張志新(化名)和其女友劉穎努潘,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體坤学,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡疯坤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了深浮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片压怠。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖飞苇,靈堂內(nèi)的尸體忽然破棺而出菌瘫,到底是詐尸還是另有隱情,我是刑警寧澤布卡,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布雨让,位于F島的核電站,受9級(jí)特大地震影響忿等,放射性物質(zhì)發(fā)生泄漏栖忠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一这弧、第九天 我趴在偏房一處隱蔽的房頂上張望娃闲。 院中可真熱鬧,春花似錦匾浪、人聲如沸皇帮。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)属拾。三九已至,卻和暖如春冷溶,著一層夾襖步出監(jiān)牢的瞬間渐白,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工逞频, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纯衍,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓苗胀,卻偏偏與公主長(zhǎng)得像襟诸,于是被迫代替她去往敵國(guó)和親瓦堵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355