原文連結
部分譯文是按自己的理解翻譯的,如有錯漏,還請指正
簡介
每天我們都會使用很多的應用程式,儘管他們有不同的約定,但大多數應用的設計是非常相似的。這就是為什麼許多客戶要求使用一些其他應用程式沒有的設計,使得應用程式顯得獨特和不同。
如果功能佈局要求非常定製化,已經不能由Android內建的View建立 —這時候就需要使用自定義View了。而這意味著在大多數情況下,我們將需要相當長的時間來完成它。但這並不意味著我們不應該這樣做,因為實現它是非常令人興奮和有趣的。
我最近面臨了類似的情況:我的任務是使用ViewPager
實現Android應用引導頁。不同於iOS,Android並沒有提供這樣的View,所以我不得不編寫一個自定義View來實現它。
我花了一些時間來實現它。幸運的是,時下很多開源專案都有類似可複用的View,這節省了我和其他開發者的時間。我決定基於這種View建立一個公共庫。如果你有類似的功能需求並且缺乏時間實現它,可以在github repo發現它。
繪製!
因為編寫自定義View比起普通的View更耗時,你應該只在為了實現特定的功能但沒有更簡單的方法情況下使用自定義View,或者你希望通過自定義View解決以下問題:
- 效能。如果你佈局裡面有很多View,你想通自定義View優化它,使其更輕量。
- 檢視層次結構複雜。
- 一個完全自定義的View,需要手動繪製才能實現。
如果你還沒有嘗試過編寫自定義View,這篇文章將教會你繪製扁平的自定義View的一些技巧。我將會告訴你整體的檢視結構,如何實現具體的功能,不要重犯常見的錯誤,以及實現動畫效果!
我們需要知道的第一件事 –View的生命週期。不知出於某種原因,谷歌並沒有提供View生命週期的圖表,由於開發者普遍對其有誤解,導致了一些意想不到的錯誤和問題,所以我們要認清這過程。
建構函式
每個View的生命都是從建構函式開始。而且這是一個繪製初始化,進行各種計算,設定預設值或做任何我們需要的事情很好的地方。
但是,為了使我們的View更易於使用和配置,Android提供了很有用的AttributeSet
介面。它很容易實現,而且絕對值得花時間去了解和實現它,因為它會幫助你(和你的團隊)通過靜態引數來設定View,對於以後新特性加入或者新螢幕擴充性支援也更好。
首先,建立一個新的檔案attrs.xml
。所有不同的自定義View屬性都可以放在該檔案中。正如你看到的這個例子,我們有一個PageIndicatorView和它的唯一屬性piv_count。
緊接著在View的建構函式中,你需要獲取這個屬性並使用它,如下圖所示。
1 2 3 4 5 6 |
public PageIndicatorView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.PageIndicatorView); int count = typedArray.getInt(R.styleable.PageIndicatorView_piv_count,0); typedArray.recycle(); } |
注意:
- 在建立自定義屬性使用一個簡單的字首,以避免與其它View類似的屬性名稱衝突。一般我們使用View名稱縮寫,就像例子中的piv_。
- 如果你使用的是Android Studio,一旦你使用完屬性,lint會建議你呼叫
recycle()
方法 。The reason is just to get rid of inefficiently bound data that’s not gonna be used again。[譯者注:翻譯有點拗口,其實就是回收TypedArray,以便後面重用]
onAttachedToWindow
父View呼叫addView(View)
後,這個View將被依附到一個視窗。在這個階段,我們的View會知道它被包圍的其他view。如果你的View和其他View在相同的layout.xml
,這是通過id找到他們的好地方(你可以通過屬性進行設定),同時可以儲存為全域性(如果需要)的引用。
onMeasure
這意味著我們的自定義View到了處理自己的大小的時候。這是非常重要的方法,因為在大多數情況下,你的View需要有特定的大小以適應你的佈局。
當你重寫此方法,需要記得的是,最終要設定setMeasuredDimension(int width, int height)
。
當處理自定義View的大小時候,使用者可能通過layout.xml
或者動態設定了具體的大小。要正確地計算它,我們需要做幾件事情。
1.計算你的View內容所需的大小(寬度和高度)。
2.獲取你的View MeasureSpec大小和模式(寬度和高度)。
1 2 3 4 5 6 |
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); } |
3.檢查MeasureSpec 設定和調整View(寬度和高度)的尺寸模式。
1 2 3 4 5 6 7 8 |
int width; if (widthMode == MeasureSpec.EXACTLY) { width = widthSize; } else if (widthMode == MeasureSpec.AT_MOST) { width = Math.min(desiredWidth, widthSize); } else { width = desiredWidth; } |
注意:
看看MeasureSpec的值:
- MeasureSpec.EXACTLY 意味著硬編碼大小值,所以你應該設定指定的寬度或高度。
- MeasureSpec.AT_MOST 用於表明你的View匹配父View的大小,
所以它應該和他想要的大小一樣大。
[譯者注:此時View尺寸只要不超過父View允許的最大尺寸即可] - MeasureSpec.UNSPECIFIED 實際上是檢視包裝尺寸。因此,你可以使用上面計算所需的大小。
在通過setMeasuredDimension
設定最終值之前,以防萬一,可以檢查這些值不為負數。這可以避免在佈局預覽時一些問題。
onLayout
此方法分配大小和位置給它的每一個子View。正因為如此,我們正在研究一個扁平的自定義檢視(繼承簡單的View)不具有任何子View,那麼就沒有理由重寫此方法。[譯者注:實現可以參考Custom Layouts on Android]
onDraw
這就是發生魔法的地方。在這裡,使用Canvas
和Paint
物件你將可以畫任何你需要的東西。
一個Canvas
例項從onDraw引數得來,它一般用於繪製不同形狀,而Paint
物件定義形狀顏色。簡單地說,Canvas
用於繪製物件,而Paint
用於造型。它們無處不在,無論繪製的是一個直線,圓或長方形。
使自定義View,要始終牢記onDraw會花費大量的時間。當佈局有一些變化,滾動、快速滑動都會導致重新繪製。所以這就是為什麼Android Studio也建議:避免在onDraw中進行物件分配的操作,物件應該只建立一次並在將來重用。
onDraw() — Paint object recreation
注意:
- 在執行繪製時始終牢記重用物件,而不建立新的。不要依賴於IDE高亮一個潛在的問題,而是自己有意識地去做這件事,因為在onDraw呼叫一個內部會建立物件的方法時,IDE無法識別它。
- 同時請不要硬編碼View大小。其他開發者在使用時可以定義不同的大小,所以View大小應該取決於它有什麼尺寸。
View 更新
從View的生命週期圖可以得知,可以重繪View自身有兩種方法。invalidate()
和requestLayout()
方法會幫助你在執行時動態改變View狀態。但為什麼需要兩個方法?
invalidate()
用來簡單重繪View。例如更新其文字,色彩或觸控互動性。View將只呼叫onDraw()
方法再次更新其狀態。requestLayout()
方法,你可以看到其將會從`onMeasure()開始更新View。這意味著你的View更新後,它改變它的大小,你需要再次測量它,並依賴於新的大小來重新繪製。
動畫
在自定義View中,動畫是一幀一幀的過程。這意味著,如果你想使一個圓半徑從小變大,你將需要逐步增加半徑並呼叫invalidate()
來重繪它。
在自定義View動畫中,ValueAnimator是你的好朋友。下面這個類將幫助你從任何值開始執行動畫到最後,甚至支援Interpolator
(如果需要)。
1 2 3 4 5 6 7 8 |
ValueAnimator animator = ValueAnimator.ofInt(0, 100); animator.setDuration(1000); animator.setInterpolator(new DecelerateInterpolator()); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { public void onAnimationUpdate(ValueAnimator animation) { int newRadius = (int) animation.getAnimatedValue(); } }); |
注意:
當每一次新的動畫值出來時,不要忘記呼叫invalidate()
。
希望這篇文章可以幫助你實現你的第一個自定義View,如果你想更多地瞭解它,可以看看這個視訊。