標籤:
郭孝星
微博:郭孝星的新浪微博
郵箱:[email protected]
部落格:http://blog.csdn.net/allenwells
Github:https://github.com/AllenWells
設計良好的類總是相似的,它使用一個易用的介面來封裝一個特定的功能,它能有效使用CPU和記憶體,我們在設計View類時,通常會考慮以下因素:
- 遵循Android標準規則
- 提供自訂的風格屬性值並能夠被Android XML Layout所識別。
- 發出可訪問的事件
- 能夠相容Android的不同平台
下面我們就來介紹如何一步步的去實現一個設計良好的類。
一 繼承一個View類
Android Framework裡的View類都繼承於View,我們自訂的View可以直接繼承View或者其他View的子類。為了能夠讓ADT識別我們的View,我們必須至少提供一個構造器,如下所示:
class PieChart extends View { public PieChart(Context context, AttributeSet attrs) { super(context, attrs); }}
二 定義自設屬性
為了添加一個內建的View到UI上,我們需要通過XML屬性來指定它的樣式和行為,良好的自訂View可以通過XML添加和改變樣式,為了達到這種效果,我們通常會考慮:
- 為自訂的View在資源標籤下定義自設的屬性
- 在XML Layout中指定屬性值
- 在運行時獲得屬性值
- 把擷取到的屬性值應用到自訂的View上
定義自設屬性,添加到res/values/attrs.xml檔案中,如下所示:
<resources> <declare-styleable name="PieChart"> <attr name="showText" format="boolean" /> <attr name="labelPosition" format="enum"> <enum name="left" value="0"/> <enum name="right" value="1"/> </attr> </declare-styleable></resources>
以上定義了兩個自設屬性:showText和labelPosition,它們都歸屬於PieChat的項目下的styleable執行個體,styleable執行個體的名字通常和自訂View的名字一致。
當我們定義了自設的屬性,我們就可以在Layout XML檔案中使用它們,就像內建屬性一樣,唯一不同時自設屬性歸屬於不容的命名空間,如下所示:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews"> <com.example.customviews.charting.PieChart custom:showText="true" custom:labelPosition="left" /></LinearLayout>
注意:
- 為了避免輸入長串的namespace名字,樣本上面使用了 xmlns 指令,這個指令可以指派custom作為 http://schemas.android.com/apk/res/com.example.customviews namespace的別名。我們也可以使用其他別名作為namespace。
- 如果你的view是一個Inner Class,我們需要指定這個View的Outer Class。同樣的,如果PieChart有一個Inner Class叫做PieView。為了使用這個類中自設的屬性,我們需要使用com.example.customviews.charting.PieChart$PieView。
三 應用自設屬性
當View從XML Layout被建立的時候,在XML標籤下的屬性值都是從res下讀取出來並傳遞到View的構造器作為一個AttributeSet的參數,儘管可以從AttributeSet中直接讀取數值,但這樣做有以下弊端:
- 擁有的屬性資源並沒有經過解析
- styles並沒有應用上
我們通過attrs的方法是可以直接擷取到屬性值的,但是不能確定值的類型,如下所示:
//通過此方法可以擷取title的值,但是不知道它的類型,處理起來很容易出問題。String title = attrs.getAttributeValue(null, "title");int resId = attrs.getAttributeResourceValue(null, "title", 0);title = context.getText(resId));
取而代之的方法是通過obtainStyledAttributes()方法來擷取屬性值,該方法會傳遞一個TypedArray對象,Android資源編譯器對res目錄裡的每一個,自動產生R.java檔案定義了存放屬性ID的數組和常量,這些常量用來引用數組中的每個屬性。我們可以通過TypedArray對象來讀取這些屬性。
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(); }}
注意:TypedArray對象是一個共用對象,使用完畢後應該進行回收。
四 添加屬性和事件
Attributes是一個強大的控制View行為和外觀的方法,但是它僅僅能夠在View被初始化的時候被讀取到,為了提供一個動態行為,我們需要設定一些set和get方法,如下所示:
public boolean isShowText() { return mShowText;}public void setShowText(boolean showText) { mShowText = showText; //invalidate()和requestLayout()兩個方法的調用是確保穩定啟動並執行關鍵。當 //View的某些內容發生變化的時候,需要調用invalidate來通知系統對這個View //進行redraw,當某些元素變化會引起組件大小變化時,需要調用requestLayout //方法。調用時若忘了這兩個方法,將會導致hard-to-find bugs。 invalidate(); requestLayout();}
除了暴露屬性之外,我們還需要暴露事件,自訂的View也需要能夠支援響應事件的監聽器。
五 繪製View的外觀5.1 重寫onDraw()方法5.1.1 建立繪製對象
繪製一個自訂View的外觀最重要的步驟是重寫onDraw(),onDraw()的參數是一個Canvas對象,Canvas對象定義了繪製文本、線條、映像和許多其他圖形的方法。
onDraw()方法會做以下常見操作:
- 繪製文字使用drawText()。指定字型通過調用setTypeface(), 通過setColor()來設定文字顏色.
- 繪製基本圖形使用drawRect(), drawOval(), drawArc(). 通過setStyle()來指定形狀是否需要filled, outlined.
- 繪製一些複雜的圖形,使用Path類. 通過給Path對象添加直線與曲線, 然後使用drawPath()來繪製圖形. 和基本圖形一樣,。是outlined, filled, both.
- 通過建立LinearGradient對象來定義漸層。調用setShader()來使用LinearGradient。
- 通過使用drawBitmap來繪製圖片.
舉例
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);}
Android Graphics Framework把繪製定義為下面兩類:
舉例
建立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));
5.1.2 處理布局事件
為了正確的繪製自訂的View,我們需要知道View的大小。複雜的自訂View通常需要根據在螢幕上的大小與形狀執行多次layout計算。而不是假設這個view在螢幕上的顯示大小。即使只有一個程式會使用自訂View,仍然是需要處理螢幕大小不同,密度不同,方向不同所帶來的影響。
View中有很多方法可以用來計算大小。
onSizeChanged():當View第一次被賦予一個大小時,或者View的大小被更改時觸發該方法,我們可以在該方法裡計算位置、間距和其他View的大小值。
當我們的View被設定大小時,布局管理器會假定這個大小包括所有View的內邊距(Padding),當我們計算View的大小時,我們需要處理內邊距的值,如下所示:
// Account for paddingfloat xpad = (float)(getPaddingLeft() + getPaddingRight());float ypad = (float)(getPaddingTop() + getPaddingBottom());// Account for the labelif (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的大小,該方法的參數是View.MeaureSpec,該參數會告知我們的View的父控制項的大小。
@Overrideprotected 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);}
注意:
- 計算的過程有把view的padding考慮進去。這個在後面會提到,這部分是view所控制的。
- 協助方法resolveSizeAndState()是用來建立最終的寬高值的。這個方法會通過比較view的需求大小與spec值,返回一個合適的View.MeasureSpec值,並傳遞到onMeasure方法中。
- onMeasure()沒有傳回值。它通過調用setMeasuredDimension()來擷取結果。調用這個方法是強制執行的,如果我們遺漏了這個方法,會出現運行時異常。
六 處理輸入手勢
Android提供一個輸入事件的模型,使用者的動作會轉換成觸發一些回呼函數的事件,我們可以通過重寫這些回調方法來處理使用者的餓輸入事件。
常見的使用者輸入事件時Touch事件,多種Touch事件之間的相互作用稱為Gesture,常見的Gesture有以下幾種:
- tapping
- pulling
- flinging
- zooming
GestureDetector用來管理Gesture,它通過傳入的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());
不管我們是否使用GestureDetector.SimpleOnGestureListener, 我們總是必須實現onDown()方法,並返回true。因為所有的gestures都是從onDown()開始的。如果你在onDown()裡面返回false,系統會認為我們想要忽略後續的gesture,那麼GestureDetector.OnGestureListener的其他回調方法就不會被執行到了。
一旦我們實現了GestureDetector.OnGestureListener並且建立了GestureDetector的執行個體, 我們可以使用我們的GestureDetector來中止你在onTouchEvent裡面收到的touch事件,如下所示:
@Overridepublic boolean onTouchEvent(MotionEvent event) { boolean result = mDetector.onTouchEvent(event); if (!result) { if (event.getAction() == MotionEvent.ACTION_UP) { stopScrolling(); result = true; } } return result;}
七 最佳化View效能7.1 提升方法效率
為了設計良好的View,我們的View應該能執行的更快,不出現卡頓,動畫也應該保持在60fps。為了加速我們的View,對於頻繁調用的方法,應該盡量減少不必要的方法,在初始化或者動畫間隙做記憶體非配的工作。
下面我們來討論如何提升一些常見方法的效率。
onDraw()方法,我們應該盡量減少onDraw()方法的調用,也即invalidate()方法的調用,如果真的有需求調用invalidate()方法,也應該調用帶參數的invalidate()方法進行精確繪製,而不是無參數的invalidate()方法,因為無參數的invalidate()方法會繪製整個View。
requestLayout()方法,會使得Android UI系統去遍曆整個View的層級來計算出每一個view的大小。如果找到有衝突的值,它會需要重新計算好幾次。另外需要盡量保持View的層級是扁平化的,這樣對提高效率很有協助。如果去設計一個複雜的UI,我們應該考慮寫一個自訂的ViewGroup來執行它的layout操作。與內建的View不同,自訂的View可以使得程式僅僅測量這一部分,這避免了遍曆整個View的層級結構來計算大小。
7.2 使用硬體加速
從Android 3.0開始,Android的2D映像系統可以通過GPU (Graphics Processing Unit)來加速。GPU硬體加速可以提高許多程式的效能。但是這並不是說它適合所有的程式。Android Framework讓我們能夠隨意控制你的程式的各個部分是否啟用硬體加速。
一旦你開啟了硬體加速,效能的提示並不一定可以明顯察覺到。行動裝置的GPU在某些例如scaling,rotating與translating的操作中表現良好。但是對其他一些任務,比如畫直線或曲線,則表現不佳。為了充分發揮GPU加速,我們應該最大化GPU擅長的操作的數量,最小化GPU不擅長操作的數量。
舉例
繪製pie是相對來說比較費時的。解決方案是把pie放到一個子View中,並設定View使用LAYER_TYPE_HARDWARE來進行加速。
private class PieView extends View { public PieView(Context context) { super(context); if (!isInEditMode()) { setLayerType(View.LAYER_TYPE_HARDWARE, null); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); for (Item it : mData) { mPiePaint.setShader(it.mShader); canvas.drawArc(mBounds, 360 - it.mEndAngle, it.mEndAngle - it.mStartAngle, true, mPiePaint); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { mBounds = new RectF(0, 0, w, h); } RectF mBounds; }
著作權聲明:當我們認真的去做一件事的時候,就能發現其中的無窮樂趣,豐富多彩的技術宛如路上的風景,邊走邊欣賞。
【Android應用開發技術:使用者介面】自訂View類設計