【Android應用開發技術:使用者介面】自訂View類設計

來源:互聯網
上載者:User

標籤:

郭孝星
微博:郭孝星的新浪微博
郵箱:[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>

注意

  1. 為了避免輸入長串的namespace名字,樣本上面使用了 xmlns 指令,這個指令可以指派custom作為 http://schemas.android.com/apk/res/com.example.customviews namespace的別名。我們也可以使用其他別名作為namespace。
  2. 如果你的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把繪製定義為下面兩類:

  • Canvas:繪製什麼
  • Paint:如何繪製

舉例

建立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()

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()

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()方法,我們應該盡量減少onDraw()方法的調用,也即invalidate()方法的調用,如果真的有需求調用invalidate()方法,也應該調用帶參數的invalidate()方法進行精確繪製,而不是無參數的invalidate()方法,因為無參數的invalidate()方法會繪製整個View。

  • requestLayout()方法

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類設計

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.