Android自定義View

一個設計良好的自定義視圖非常類似於其他任何精心設計的類。

它封裝了一組特定的功能和一個易於使用的界面,它高效使用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硬解加速,在位移,放大縮小,翻轉圖像時有極大的性能提升,但比如畫線條,曲線時就沒有明顯的效果。





發佈了32 篇原創文章 · 獲贊 1 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章