一個設計良好的自定義視圖非常類似於其他任何精心設計的類。
它封裝了一組特定的功能和一個易於使用的界面,它高效使用CPU和內存,等等。除了是一個精心設計的類,一個定製的視圖應該:
符合Android標準
提供定製styleable屬性使用Android XML佈局
發送訪問事件
與多個Android平臺兼容。
一、創建自定義view的java類
1.創建一個自定義View,AView繼承View或者View的子類,比如Button。如果你要自定義一個按鈕那麼比較好的做法就是繼承一個Button。
2.如果我們希望和系統的Button一樣可以通過xml來設置(如果不需要這一步可能省略)。
2.1 Aview中要提供至少一個帶Context和AttributeSet參數的構造方法(在xml中定義的屬性,比如text=”test“,系統會讀取xml並把數據加載到AttributeSet中)
class PieChart extends View { public PieChart(Context context, AttributeSet attrs) { super(context, attrs); } }2.2 按照如下代碼編寫構造方法,雖然AttributeSet中可以直接讀取配置的值,但是Style與應用資源的屬性值沒有解決(比如屬性值的類型等)。
public PieChart(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.getTheme().obtainStyledAttributes( attrs, R.styleable.PieChart, 0, 0); try { mShowText = a.getBoolean(R.styleable.PieChart_showText, false); mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0); } finally { a.recycle(); } }3. 提供View屬性的get,set方法比如:
public boolean isShowText() { return mShowText; } public void setShowText(boolean showText) { mShowText = showText; invalidate(); requestLayout(); }Tips:
(特別注意,當set方法中改變的屬性需要刷新View的話,比如形狀改變,文字改變等,一定要在變化後調用 invalidate(); requestLayout();通知系統重繪View,如果忘記了,就會導致一些很難定位的Bug)
4.提供View的事件。比如
protected
void onSelectionChanged(int selStart, int selEnd) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
}
Tips:
當使用自定義View的時候是非常容易遺漏事件的,因爲你當時可能只用到了幾個事件,而後期你需要處理新事件時,就如要維護這個定製View,增加新的事件。一個好的做法就是抽出一點時間,把可能要用的事件,儘可能多的提供出來。
二、定製View的圖形
要實現定製View有許多方法可以重寫,這裏提供幾個最常用,最基本的繪圖相關方法;
1.重寫onDraw()方法
畫一個定製的視圖中最重要的一步是重寫onDraw()方法。onDraw()的一個參數Canvas是一個畫布對象,可以用來畫View本身。
畫布上可以繪製文本,線,位圖,和許多其他的基本圖形。您可以使用onDraw()提供的方法來創建您的自定義用戶界面(UI)。
在繪畫View之前,我們還需要一支筆Paitn()方法。後面會有詳細的解釋。
2.創建繪圖對象
Canvas:畫什麼
Paint:怎麼畫。
比如畫一個矩形(使用Canvas),矩形的線條顏色,是否用顏色填滿該矩形(使用Paint)。創建方式如下:
private void init() { mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setColor(mTextColor); if (mTextHeight == 0) { mTextHeight = mTextPaint.getTextSize(); } else { mTextPaint.setTextSize(mTextHeight); } mPiePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPiePaint.setStyle(Paint.Style.FILL); mPiePaint.setTextSize(mTextHeight); mShadowPaint = new Paint(0); mShadowPaint.setColor(0xff101010); mShadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL)); ...初始canvas與Paint不要放在再onDraw()方法中,因爲onDraw()方法在View改變時,會多次被回調。每一次都初始化一次,顯然會多創建很多的對象,多費時間,這會影響View重繪的速度,從而影響用戶體驗。
三、處理佈局事件
如何繪製view已經在上面說了,那麼View的可繪畫區域的大小是多少呢?這不光和view本身有關,還和View所在佈局有關。比如橫屏與豎屏時。一些負責的View中還包含了其他的View。這樣如何來計算View的大小呢。有很多的方法,但通常只需要重寫onSizeChanged()
和onMeasure()方法,其他用默認的就行。
onSizeChanged()方法在View第一次分配到size的時候和之後任何原因引起的大小改變,都會回調這個方法。這個size是包括了View的Padding,因此需要計算view中圖形的大小還要計算Padding的大小。比如:
// Account for padding float xpad = (float)(getPaddingLeft() + getPaddingRight()); float ypad = (float)(getPaddingTop() + getPaddingBottom()); // Account for the label if (mShowText) xpad += mTextWidth; float ww = (float)w - xpad; float hh = (float)h - ypad; // Figure out how big we can make the pie. float diameter = Math.min(ww, hh);onMeasure()方法的一個參數
View.MeasureSpec
會告訴你View所在的父View對你View的要求,比如是否給了一個硬性的大小,或者是建議你儘可能的佔滿空間。這些要求都是通過 View.MeasureSpec
返回。用法如下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Try for a width based on our minimum int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth(); int w = resolveSizeAndState(minw, widthMeasureSpec, 1); // Whatever the width ends up being, ask for a height that would let the pie // get as big as it can int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop(); int h = resolveSizeAndState(MeasureSpec.getSize(w) - (int)mTextWidth, heightMeasureSpec, 0); setMeasuredDimension(w, h); }Tips:
1.覆寫onMeasure方法的時候,子類有責任確保measured height and width至少爲這個View的最小height和width。
2. 有一個約定:在覆寫onMeasure方法的時候,必須調用 setMeasuredDimension(int,int)來存儲這個View經過測量得到的measured width and height。如果沒有這麼做,將會由measure(int, int)方法拋出一個運行時異常。
實際如何繪製圖形呢,以下給了一些參考代碼:
protected void onDraw(Canvas canvas) { super.onDraw(canvas); // Draw the shadow canvas.drawOval( mShadowBounds, mShadowPaint ); // Draw the label text canvas.drawText(mData.get(mCurrentItem).mLabel, mTextX, mTextY, mTextPaint); // Draw the pie slices for (int i = 0; i < mData.size(); ++i) { Item it = mData.get(i); mPiePaint.setShader(it.mShader); canvas.drawArc(mBounds, 360 - it.mEndAngle, it.mEndAngle - it.mStartAngle, true, mPiePaint); } // Draw the pointer canvas.drawLine(mTextX, mPointerY, mPointerX, mPointerY, mTextPaint); canvas.drawCircle(mPointerX, mPointerY, mPointerSize, mTextPaint); }四、View與用戶的交互
以上我們繪製了view,也讓view顯示出來了。但看見的同時,往往還需要響應一些用戶的操作。即交互。
如何定義這些交互呢。通常就是儘可能的貼近現實,比如圖片應該是從在移動到那,而不是從這消失,從那突然出現。
這裏我們採用系統框架提供的事件(單擊,長按等),能滿足巨大多數的要求。其中複雜一點就是手勢事件,比如用戶滑動手指。
4.1 處理手勢事件
在View上用戶進行操作時都會觸發View中的onTouchEvent(android.view.MotionEvent)
.其中的MotionEvent中包含了用戶操作的所有信息,比如用戶在屏幕什麼位置點擊了,滑動到哪裏了。直接用這些數據比較麻煩(比如根據這些數據判斷用戶滑動了沒有),Android提供了GestureDetector
.手勢檢測類,使用這個類需要實現一個監聽GestureDetector.OnGestureListener。如果不需要那麼多手勢的話可以使用GestureDetector.SimpleOnGestureListener。實例代碼:
class mListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDown(MotionEvent e) { return true; } } mDetector = new GestureDetector(PieChart.this.getContext(), new mListener());Tips:
public boolean onDown(MotionEvent e) 返回true就代表事件已經處理完畢,不用再處理了。把MotionEvent傳給GestureDetector,GestureDetector會自動調用對應的手勢事件。如果不能識別的話,就會返回false;示例代碼:
@Override public boolean onTouchEvent(MotionEvent event) { boolean result = mDetector.onTouchEvent(event); if (!result) { if (event.getAction() == MotionEvent.ACTION_UP) { stopScrolling(); result = true; } } return result; }4.2 手勢命令高級技巧
要貼近人類的思維。比如滑動時,開始慢慢加速,到最後減速,就想物理中的飛輪一樣。如果把MotionEvent變爲飛輪來處理。是需要數學模型與複雜的計算。當然Android中可以直接使用Scroller
這個幫助類來完成。設置初始速度,每次最少移動和最多移動。其他的比如飛輪效果,由Scroller來完成就好。可惜Scroller只會幫你計算出某一時刻,應該要運動到位置X和Y。所以爲什麼看起來連貫,你需要週期行的調用 Scroller.computeScrollOffset(),
getCurrX(),
getCurrY()
來獲取當前的位置X和Y。然後通過 scrollTo()方法讓view真正移動起來。但是View的移動還是很生硬,比如從X=1到X=5是突然消失,突然出現的。因此爲了順滑,通常使用
ValueAnimator(在3.0以後才提供)。
@Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { mScroller.fling(currentX, currentY, velocityX / SCALE, velocityY / SCALE, minX, minY, maxX, maxY); postInvalidate(); }4.3 讓view更順滑
Android
3.0可以使用Android property
animation framework,屬性動畫框架,完成。
五、優化View
5.1 在經常被回調的方法中,儘可能的簡化代碼,不要在回調方法中不斷的創建對象,要放到初始化方法中。經常被回調的方法有:
onDraw(),動畫進行過程中的一些方法。
5.2 儘可能的減少view的層次,因爲測量View的方法也會被經常調用,會從上往下調用,這樣如果層次很多,或者view測量中view之間有衝突,會照成測量多次調用,極大的影響了性能。
5.3 android 3.0以上支持GPU硬解加速,在位移,放大縮小,翻轉圖像時有極大的性能提升,但比如畫線條,曲線時就沒有明顯的效果。