Android 學(xué)習(xí)筆記之自定義 View

本文整理自: Google 官方文檔之自定義 View,筆者省略了對(duì)自己幫助不大的章節(jié),拜讀原文請(qǐng)點(diǎn)鏈接。

一、繼承一個(gè)View

Android Framework 里面定義的 View 類都繼承自 View 。你自定義的 View 也可以直接繼承 View,或者你可以通過(guò)繼承既有的一個(gè)子類(例如 Button )來(lái)節(jié)約一點(diǎn)時(shí)間。
為了讓 Android Developer Tools 能夠識(shí)別你的 View,你必須至少提供一個(gè) constructor,它包含一個(gè) Context 與一個(gè) AttributeSet 對(duì)象作為參數(shù)。這個(gè) constructor 允許 layout editor 創(chuàng)建并編輯你的 View 的實(shí)例。

class PieChart extends View {
    public PieChart(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

1.定義自定義屬性

為了添加一個(gè)內(nèi)置的 View 到你的 UI 上,你需要通過(guò) XML 屬性來(lái)指定它的樣式與行為。良好的自定義 Views 可以通過(guò) XML 添加和改變樣式,為了讓你的自定義的 View 也有如此的行為,你應(yīng)該:

  • 為你的 View 在資源標(biāo)簽下定義自設(shè)的屬性
  • 在你的 XML layout 中指定屬性值
  • 在運(yùn)行時(shí)獲取屬性值
  • 把獲取到的屬性值應(yīng)用在你的 View 上

為了定義自設(shè)的屬性,添加 資源到你的項(xiàng)目中。放置于 res/values/attrs.xml 文件中。下面是一個(gè) 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>

上面的代碼聲明了 2 個(gè)自設(shè)的屬性,showText 與 labelPosition,它們都?xì)w屬于 PieChart 的項(xiàng)目下的 styleable 實(shí)例。styleable 實(shí)例的名字,通常與自定義的 View 名字一致。盡管這并沒(méi)有嚴(yán)格規(guī)定要遵守這個(gè) convention,但是許多流行的代碼編輯器都依靠這個(gè)命名規(guī)則來(lái)提供 statement completion。

一旦你定義了自設(shè)的屬性,你可以在 layout XML 文件中使用它們,就像內(nèi)置屬性一樣。唯一不同的是你自設(shè)的屬性是歸屬于不同的命名空間。不是屬于「 http://schemas.android.com/apk/res/android 」 的命名空間,它們歸屬于「 http://schemas.android.com/apk/res/你的包名 」。例如,下面演示了如何為 PieChart 使用上面定義的屬性:

<?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>

為了避免輸入長(zhǎng)串的 namespace 名字,示例上面使用了 xmlns 指令,這個(gè)指令可以指派 custom 作為「http://schemas.android.com/apk/res/com.example.customviewsnamespace 」的別名。你也可以選擇其他的別名作為你的 namespace。請(qǐng)注意,如果你的 View 是一個(gè) inner class,你必須指定這個(gè) View 的 outer class。同樣的,如果 PieChart 有一個(gè) inner class 叫做 PieView 。為了使用這個(gè)類中自設(shè)的屬性,你應(yīng)該使用 com.example.customviews.charting.PieChart$PieView.

2.應(yīng)用自定義屬性

當(dāng) View 從 XML layout 被創(chuàng)建的時(shí)候,在 xml 標(biāo)簽下的屬性值都是從 resource 下讀取出來(lái)并傳遞到 View 的 constructor 作為一個(gè) AttributeSet 參數(shù)。盡管可以從 AttributeSet 中直接讀取數(shù)值,可是這樣做有些弊端:

  • 擁有屬性的資源并沒(méi)有經(jīng)過(guò)解析
  • Styles 并沒(méi)有運(yùn)用上

翻譯注:通過(guò) attrs 的方法是可以直接獲取到屬性值的,但是不能確定值類型,如:

String title = attrs.getAttributeValue(null, "title");
int resId = attrs.getAttributeResourceValue(null, "title", 0);
title = context.getText(resId));

都能獲取到 "title" 屬性,但你不知道值是字符串還是 resId,處理起來(lái)就容易出問(wèn)題,下面的方法則能在編譯時(shí)就發(fā)現(xiàn)問(wèn)題

取而代之的是,通過(guò) obtainStyledAttributes() 來(lái)獲取屬性值。這個(gè)方法會(huì)傳遞一個(gè) TypedArray 對(duì)象,它是間接 referenced 并且 styled 的。

Android資源編譯器幫你做了許多工作來(lái)使調(diào)用 [obtainStyledAttributes()](http://developer.android.com/reference/android/content/res/Resources.Theme.html#obtainStyledAttributes(android.util.AttributeSet, int[], int, int)) 更簡(jiǎn)單。對(duì) res 目錄里的每一個(gè) <declare-styleable>
資源,自動(dòng)生成的 R.java 文件定義了存放屬性 ID 的數(shù)組和常量,常量用來(lái)索引數(shù)組中每個(gè)屬性。你可以使用這些預(yù)先定義的常量來(lái)從
TypedArray 中讀取屬性。這里就是 PieChart 類如何讀取它的屬性:

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 對(duì)象是一個(gè)共享資源,必須被在使用后進(jìn)行回收。

3.添加屬性和事件

Attributes 是一個(gè)強(qiáng)大的控制view的行為與外觀的方法,但是他們僅僅能夠在 View 被初始化的時(shí)候被讀取到。為了提供一個(gè)動(dòng)態(tài)的行為,需要暴露出一些合適的 getter 與 setter 方法。下面的代碼演示了如何使用這個(gè)技巧:

public boolean isShowText() {
   return mShowText;
}

public void setShowText(boolean showText) {
   mShowText = showText;
   invalidate();
   requestLayout();
}

請(qǐng)注意,在 setShowText 方法里面有調(diào)用 invalidate()
and requestLayout()。 這兩個(gè)調(diào)用是確保穩(wěn)定運(yùn)行的關(guān)鍵。當(dāng) View 的某些內(nèi)容發(fā)生變化的時(shí)候,需要調(diào)用 invalidate 來(lái)通知系統(tǒng)對(duì)這個(gè) View 進(jìn)行 redraw,當(dāng)某些元素變化會(huì)引起組件大小變化時(shí),需要調(diào)用 requestLayout 方法。調(diào)用時(shí)若忘了這兩個(gè)方法,將會(huì)導(dǎo)致 hard-to-find bugs。
自定義的 View 也需要能夠支持響應(yīng)事件的監(jiān)聽(tīng)器。例如,PieChart 暴露了一個(gè)自定義的事件 OnCurrentItemChanged 來(lái)通知監(jiān)聽(tīng)器,用戶已經(jīng)切換了焦點(diǎn)到一個(gè)新的組件上。我們很容易忘記了暴露屬性與事件,特別是當(dāng)你是這個(gè) View 的唯一用戶時(shí)。請(qǐng)花費(fèi)一些時(shí)間來(lái)仔細(xì)定義你的
View 的交互。一個(gè)好的規(guī)則是總是暴露任何屬性與事件。


二、實(shí)現(xiàn)自定義 View 的繪制

重繪一個(gè)自定義的 View 的最重要的步驟是重寫(xiě) onDraw() 方法。onDraw() 的參數(shù)是一個(gè) Canvas 對(duì)象。Canvas 類定義了繪制文本,線條,圖像與許多其他圖形的方法。你可以在 onDraw 方法里面使用那些方法來(lái)創(chuàng)建你的 UI。

在你調(diào)用任何繪制方法之前,你需要?jiǎng)?chuàng)建一個(gè) Paint 對(duì)象。

1.創(chuàng)建繪圖對(duì)象

android.graphics framework 把繪制定義為下面兩類:

  • 繪制什么,由 Canvas 處理
  • 如何繪制,由 Paint 處理

例如 Canvas 提供繪制一條直線的方法,Paint 提供直線顏色。Canvas 提供繪制矩形的方法,Paint 定義是否使用顏色填充。簡(jiǎn)單來(lái)說(shuō):Canvas 定義你在屏幕上畫(huà)的圖形,而 Paint 定義顏色,樣式,字體,所以在繪制之前,你需要?jiǎng)?chuàng)建一個(gè)或者多個(gè) Paint 對(duì)象。在這個(gè) PieChart 的例子,是在 init() 方法實(shí)現(xiàn)的,由 constructor 調(diào)用。

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

   ...

剛開(kāi)始就創(chuàng)建對(duì)象是一個(gè)重要的優(yōu)化技巧。Views 會(huì)被頻繁的重新繪制,初始化許多繪制對(duì)象需要花費(fèi)昂貴的代價(jià)。在 onDraw 方法里面創(chuàng)建繪制對(duì)象會(huì)嚴(yán)重影響到性能并使得你的 UI 顯得卡頓。

2.處理布局事件

為了正確的繪制你的 View,你需要知道 View 的大小。復(fù)雜的自定義 View 通常需要根據(jù)在屏幕上的大小與形狀執(zhí)行多次 layout 計(jì)算。而不是假設(shè)這個(gè) View 在屏幕上的顯示大小。即使只有一個(gè)程序會(huì)使用你的 View,仍然是需要處理屏幕大小不同,密度不同,方向不同所帶來(lái)的影響。
盡管 View 有許多方法是用來(lái)計(jì)算大小的,但是大多數(shù)是不需要重寫(xiě)的。如果你的 View 不需要特別的控制它的大小,唯一需要重寫(xiě)的方法是[ onSizeChanged() ](http://developer.android.com/reference/android/view/View.html#onSizeChanged(int, int, int, int)),當(dāng)你的 View 第一次被賦予一個(gè)大小時(shí),或者你的 View 大小被更改時(shí)會(huì)被執(zhí)行。在 onSizeChanged 方法里面計(jì)算位置,間距等其他與你的 View 大小值。
當(dāng)你的 View 被設(shè)置大小時(shí),layout manager (布局管理器)假定這個(gè)大小包括所有的 View 的內(nèi)邊距 (padding) 。當(dāng)你計(jì)算你的 View 大小時(shí),你必須處理內(nèi)邊距的值。這段 PieChart.onSizeChanged() 中的代碼演示該怎么做:

       // 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);

如果你想更加精確的控制你的 View 的大小,需要重寫(xiě)[ onMeasure() ](http://developer.android.com/reference/android/view/View.html#onMeasure(int, int))方法。這個(gè)方法的參數(shù)是 View.MeasureSpec,它會(huì)告訴你的 View 的父控件的大小。那些值被包裝成 int 類型,你可以使用靜態(tài)方法來(lái)獲取其中的信息。
這里是一個(gè)實(shí)現(xiàn) onMeasure() 的例子。在這個(gè)例子中 PieChart 試著使它的區(qū)域足夠大,使pie可以像它的 label 一樣大:

@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);
}

上面的代碼有三個(gè)重要的事情需要注意:

  • 計(jì)算的過(guò)程有把 View 的 padding 考慮進(jìn)去。這個(gè)在后面會(huì)提到,這部分是 View 所控制的。
  • 幫助方法 resolveSizeAndState() 是用來(lái)創(chuàng)建最終的寬高值的。這個(gè)方法比較 View 的期望值與傳遞給 onMeasure 方法的 spec 值,然后返回一個(gè)合適的 View.MeasureSpec 值。
  • onMeasure() 沒(méi)有返回值。它通過(guò)調(diào)用 setMeasuredDimension() 來(lái)獲取結(jié)果。調(diào)用這個(gè)方法是強(qiáng)制執(zhí)行的,如果你遺漏了這個(gè)方法,會(huì)出現(xiàn)運(yùn)行時(shí)異常。

3.繪圖

每個(gè) View 的 onDraw 都是不同的,但是有下面一些常見(jiàn)的操作:

  • 繪制文字使用 drawText() 。指定字體通過(guò)調(diào)用 setTypeface() ,通過(guò) setColor() 來(lái)設(shè)置文字顏色.
  • 繪制基本圖形使用 drawRect() , drawOval() , drawArc() . 通過(guò) setStyle() 來(lái)指定形狀是否需要 filled, outlined.
  • 繪制一些復(fù)雜的圖形,使用 Path 類. 通過(guò)給 Path 對(duì)象添加直線與曲線, 然后使用 drawPath() 來(lái)繪制圖形. 和基本圖形一樣,paths 也可以通過(guò)
    setStyle 來(lái)設(shè)置是outlined, filled, both.
  • 通過(guò)創(chuàng)建 LinearGradient 對(duì)象來(lái)定義漸變。調(diào)用 setShader() 來(lái)使用 LinearGradient。
  • 通過(guò)使用 drawBitmap 來(lái)繪制圖片.
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 可交互

繪制 UI 僅僅是創(chuàng)建自定義 View 的一部分。你還需要使得你的 View 能夠以模擬現(xiàn)實(shí)世界的方式來(lái)進(jìn)行反饋。對(duì)象應(yīng)該總是與現(xiàn)實(shí)情景能夠保持一致。例如,圖片不應(yīng)該突然消失又從另外一個(gè)地方出現(xiàn),因?yàn)樵诂F(xiàn)實(shí)世界里面不會(huì)發(fā)生那樣的事情。正確的應(yīng)該是,圖片從一個(gè)地方移動(dòng)到另外一個(gè)地方。

用戶應(yīng)該可以感受到 UI 上的微小變化,并對(duì)模仿現(xiàn)實(shí)世界的細(xì)微之處反應(yīng)強(qiáng)烈。例如,當(dāng)用戶 fling (迅速滑動(dòng))一個(gè)對(duì)象時(shí),應(yīng)該在開(kāi)始時(shí)感到摩擦帶來(lái)的阻力,在結(jié)束時(shí)感到 fling 帶動(dòng)的動(dòng)力。應(yīng)該在滑動(dòng)開(kāi)始與結(jié)束的時(shí)候給用戶一定的反饋。

1.處理輸入的手勢(shì)

像許多其他 UI 框架一樣,Android 提供一個(gè)輸入事件模型。用戶的動(dòng)作會(huì)轉(zhuǎn)換成觸發(fā)一些回調(diào)函數(shù)的事件,你可以重寫(xiě)這些回調(diào)方法來(lái)定制你的程序應(yīng)該如何響應(yīng)用戶的輸入事件。在 Android 中最常用的輸入事件是 touch,它會(huì)觸發(fā) onTouchEvent(android.view.MotionEvent) 的回調(diào)。重寫(xiě)這個(gè)方法來(lái)處理 touch 事件:

@Override
public boolean onTouchEvent(MotionEvent event) {
  return super.onTouchEvent(event);
}

Touch 事件本身并不是特別有用。如今的 touch UI 定義了 touch 事件之間的相互作用,叫做 gestures 。例如 tapping,pulling,flinging 與
zooming 。為了把那些 touch 的源事件轉(zhuǎn)換成 gestures, Android 提供了 GestureDetector

通過(guò)傳入 GestureDetector.OnGestureListener 的一個(gè)實(shí)例構(gòu)建一個(gè) GestureDetector 。如果你只是想要處理幾種 gestures (手勢(shì)操作)你可以繼承 GestureDetector.SimpleOnGestureListener ,而不用實(shí)現(xiàn) GestureDetector.OnGestureListener 接口。例如,下面的代碼創(chuàng)建一個(gè)繼承 GestureDetector.SimpleOnGestureListener 的類,并重寫(xiě) onDown(MotionEvent)

class mListener extends GestureDetector.SimpleOnGestureListener {
   @Override
   public boolean onDown(MotionEvent e) {
       return true;
   }
}
mDetector = new GestureDetector(PieChart.this.getContext(), new mListener());

不管你是否使用 GestureDetector.SimpleOnGestureListener,,你必須總是實(shí)現(xiàn) onDown() 方法,并返回 true 。這一步是必須的,因?yàn)樗械?br> gestures 都是從 onDown() 開(kāi)始的。如果你在 onDown() 里面返回 false,系統(tǒng)會(huì)認(rèn)為你想要忽略后續(xù)的 gesture,那么GestureDetector.OnGestureListener 的其他回調(diào)方法就不會(huì)被執(zhí)行到了。一旦你實(shí)現(xiàn)了 GestureDetector.OnGestureListener 并且創(chuàng)建了GestureDetector 的實(shí)例, 你可以使用你的 GestureDetector 來(lái)中止你在 onTouchEven t里面收到的 touch 事件。

@Override
public boolean onTouchEvent(MotionEvent event) {
   boolean result = mDetector.onTouchEvent(event);
   if (!result) {
       if (event.getAction() == MotionEvent.ACTION_UP) {
           stopScrolling();
           result = true;
       }
   }
   return result;
}

當(dāng)你傳遞一個(gè) touch 事件到 onTouchEvent() 時(shí),若這個(gè)事件沒(méi)有被辨認(rèn)出是何種 gesture,它會(huì)返回 false 。你可以執(zhí)行自定義的 gesture-decection 代碼。

2.創(chuàng)建基本合理的物理運(yùn)動(dòng)

Gestures 是控制觸摸設(shè)備的一種強(qiáng)有力的方式,但是除非你能夠產(chǎn)出一個(gè)合理的觸摸反饋,否則將是違反用戶直覺(jué)的。一個(gè)很好的例子是 fling 手勢(shì),用戶迅速的在屏幕上移動(dòng)手指然后抬手離開(kāi)屏幕。這個(gè)手勢(shì)應(yīng)該使得 UI 迅速的按照 fling 的方向進(jìn)行滑動(dòng),然后慢慢停下來(lái),就像是用戶旋轉(zhuǎn)一個(gè)飛輪一樣。

但是模擬這個(gè)飛輪的感覺(jué)并不簡(jiǎn)單,要想得到正確的飛輪模型,需要大量的物理,數(shù)學(xué)知識(shí)。幸運(yùn)的是,Android 有提供幫助類來(lái)模擬這些物理行為。 Scroller 是控制飛輪式的 fling 的基類。

要啟動(dòng)一個(gè) fling,需調(diào)用 fling(),并傳入啟動(dòng)速率 x 、y 的最小值和最大值,對(duì)于啟動(dòng)速度值,可以使用 GestureDetector 計(jì)算得出。

@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();
}

Note: 盡管速率是通過(guò) GestureDetector 來(lái)計(jì)算的,許多開(kāi)發(fā)者感覺(jué)使用這個(gè)值使得 fling 動(dòng)畫(huà)太快。通常把 x 與 y 設(shè)置為 4 到 8 倍的關(guān)系。

調(diào)用[ fling() ](http://developer.android.com/reference/android/widget/Scroller.html#fling(int, int, int, int, int, int, int, int))時(shí)會(huì)為 fling 手勢(shì)設(shè)置物理模型。然后,通過(guò)調(diào)用定期調(diào)用 Scroller.computeScrollOffset() 來(lái)更新 Scroller 。 computeScrollOffset() 通過(guò)讀取當(dāng)前時(shí)間和使用物理模型來(lái)計(jì)算 x 和 y 的位置更新 Scroller 對(duì)象的內(nèi)部狀態(tài)。調(diào)用 getCurrX() getCurrY() 來(lái)獲取這些值。

大多數(shù) View 通過(guò) Scroller 對(duì)象的 x , y 的位置直接到[ scrollTo() ](http://developer.android.com/reference/android/view/View.html#scrollTo(int, int)),PieChart 例子稍有不同,它使用當(dāng)前滾動(dòng) y 的位置設(shè)置圖表的旋轉(zhuǎn)角度。

if (!mScroller.isFinished()) {
    mScroller.computeScrollOffset();
    setPieRotation(mScroller.getCurrY());
}

Scroller 類會(huì)為你計(jì)算滾動(dòng)位置,但是他不會(huì)自動(dòng)把哪些位置運(yùn)用到你的View 上面。你有責(zé)任確保 View 獲取并運(yùn)用到新的坐標(biāo)。你有兩種方法來(lái)實(shí)現(xiàn)這件事情:

  • 在調(diào)用 fling() 之后執(zhí)行 postInvalidate(),這是為了確保能強(qiáng)制進(jìn)行重畫(huà)。這個(gè)技術(shù)需要每次在 onDraw 里面計(jì)算過(guò) scroll offsets (滾動(dòng)偏移量)之后調(diào)用 postInvalidate()。
  • 使用 ValueAnimator 在 fling 是展現(xiàn)動(dòng)畫(huà),并且通過(guò)調(diào)用addUpdateListener() 增加對(duì) fling 過(guò)程的監(jiān)聽(tīng)。

這個(gè) PieChart 的例子使用了第二種方法。這個(gè)方法使用起來(lái)會(huì)稍微復(fù)雜一點(diǎn),但是它更有效率并且避免了不必要的重畫(huà)的 View 進(jìn)行重繪。缺點(diǎn)是 ValueAnimator 是從API Level 11 才有的。因此他不能運(yùn)用到 3.0 的系統(tǒng)之前的版本上。

Note: ValueAnimator 雖然是 API 11 才有的,但是你還是可以在最低版本低于 3.0 的系統(tǒng)上使用它,做法是在運(yùn)行時(shí)判斷當(dāng)前的 API Level,如果低于 11 則跳過(guò)。

 mScroller = new Scroller(getContext(), null, true);
 mScrollAnimator = ValueAnimator.ofFloat(0,1);
 mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
     @Override
     public void onAnimationUpdate(ValueAnimator valueAnimator) {
         if (!mScroller.isFinished()) {
             mScroller.computeScrollOffset();
             setPieRotation(mScroller.getCurrY());
         } else {
             mScrollAnimator.cancel();
             onScrollFinished();
         }
     }
 });

3.使過(guò)渡平滑

用戶期待一個(gè) UI 之間的切換是能夠平滑過(guò)渡的。UI 元素需要做到漸入淡出來(lái)取代突然出現(xiàn)與消失。Android 從 3.0 開(kāi)始有提供 property animation framework ,用來(lái)使得平滑過(guò)渡變得更加容易。

使用這套動(dòng)畫(huà)系統(tǒng)時(shí),任何時(shí)候?qū)傩缘母淖兌紩?huì)影響到你的視圖,所以不要直接改變屬性的值。而是使用 ValueAnimator 來(lái)實(shí)現(xiàn)改變。在下面的例子中,在 PieChart 中更改選擇的部分將導(dǎo)致整個(gè)圖表的旋轉(zhuǎn),以至選擇的進(jìn)入選擇區(qū)內(nèi)。ValueAnimator 在數(shù)百毫秒內(nèi)改變旋轉(zhuǎn)量,而不是突然地設(shè)置新的旋轉(zhuǎn)值。

mAutoCenterAnimator = ObjectAnimator.ofInt(PieChart.this, "PieRotation", 0);
mAutoCenterAnimator.setIntValues(targetAngle);
mAutoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION);
mAutoCenterAnimator.start();

如果你想改變的是 View 的某些基礎(chǔ)屬性,你可以使用 ViewPropertyAnimator ,它能夠同時(shí)執(zhí)行多個(gè)屬性的動(dòng)畫(huà)。

animate().rotation(targetAngle).setDuration(ANIM_DURATION).start();

四、優(yōu)化自定義 View

1.Do Less, Less Frequently

為了加速你的 View,對(duì)于頻繁調(diào)用的方法,需要盡量減少不必要的代碼。先從 onDraw 開(kāi)始,需要特別注意不應(yīng)該在這里做內(nèi)存分配的事情,因?yàn)樗鼤?huì)導(dǎo)致 GC,從而導(dǎo)致卡頓。在初始化或者動(dòng)畫(huà)間隙期間做分配內(nèi)存的動(dòng)作。不要在動(dòng)畫(huà)正在執(zhí)行的時(shí)候做內(nèi)存分配的事情。

你還需要盡可能的減少 onDraw 被調(diào)用的次數(shù),大多數(shù)時(shí)候?qū)е?onDraw 都是因?yàn)檎{(diào)用了 invalidate()。因此請(qǐng)盡量減少調(diào)用 invaildate() 的次數(shù)。如果可能的話,盡量調(diào)用含有4個(gè)參數(shù)的 invalidate() 方法而不是沒(méi)有參數(shù)的 invalidate()。沒(méi)有參數(shù)的 invalidate 會(huì)強(qiáng)制重繪整個(gè) View 。

另外一個(gè)非常耗時(shí)的操作是請(qǐng)求layout。任何時(shí)候執(zhí)行 requestLayout(),會(huì)使得 Android UI 系統(tǒng)去遍歷整個(gè) View 的層級(jí)來(lái)計(jì)算出每一個(gè) View 的大小。如果找到有沖突的值,它會(huì)需要重新計(jì)算好幾次。另外需要盡量保持 View 的層級(jí)是扁平化的,這樣對(duì)提高效率很有幫助。

如果你有一個(gè)復(fù)雜的 UI,你應(yīng)該考慮寫(xiě)一個(gè)自定義的 ViewGroup 來(lái)執(zhí)行他的 layout 操作。與內(nèi)置的 View 不同,自定義的 View 可以使得程序僅僅測(cè)量這一部分,這避免了遍歷整個(gè)view的層級(jí)結(jié)構(gòu)來(lái)計(jì)算大小。這個(gè) PieChart 例子展示了如何繼承 ViewGroup 作為自定義 View 的一部分。PieChart 有子 views,但是它從來(lái)不測(cè)量它們。而是根據(jù)他自身的 layout 法則,直接設(shè)置它們的大小。

2.使用硬件加速

從 Android 3.0 開(kāi)始,Android 的 2D 圖像系統(tǒng)可以通過(guò) GPU (Graphics Processing Unit)) 來(lái)加速。GPU 硬件加速可以提高許多程序的性能。但是這并不是說(shuō)它適合所有的程序。Android Framework 讓你能過(guò)隨意控制你的程序的各個(gè)部分是否啟用硬件加速。

參考 Android Developers Guide 中的 Hardware Acceleration 來(lái)學(xué)習(xí)如何在 application,activity,或 window 層啟用加速。注意除了 Android Guide 的指導(dǎo)之外,你必須要設(shè)置你的應(yīng)用的 target API 為 11,或更高,通過(guò)在你的 AndroidManifest.xml 文件中增加 < uses-sdk android:targetSdkVersion="11"/> 。

一旦你開(kāi)啟了硬件加速,性能的提示并不一定可以明顯察覺(jué)到。移動(dòng)設(shè)備的 GPU 在某些例如 scaling,rotating 與 translating 的操作中表現(xiàn)良好。但是對(duì)其他一些任務(wù),比如畫(huà)直線或曲線,則表現(xiàn)不佳。為了充分發(fā)揮 GPU 加速,你應(yīng)該最大化 GPU 擅長(zhǎng)的操作的數(shù)量,最小化 GPU 不擅長(zhǎng)操作的數(shù)量。

在下面的例子中,繪制 pie 是相對(duì)來(lái)說(shuō)比較費(fèi)時(shí)的。解決方案是把 pie 放到一個(gè)子 View 中,并設(shè)置 View 使用 LAYER_TYPE_HARDWARE 來(lái)進(jìn)行加速。

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;
   }

通過(guò)這樣的修改以后,PieChart.PieView.onDraw() 只會(huì)在第一次現(xiàn)實(shí)的時(shí)候被調(diào)用。之后,pie chart 會(huì)被緩存為一張圖片,并通過(guò) GPU 來(lái)進(jìn)行重畫(huà)不同的角度。GPU 特別擅長(zhǎng)這類的事情,并且表現(xiàn)效果突出。

緩存圖片到 hardware layer 會(huì)消耗 video memory,而 video memory 又是有限的。基于這樣的考慮,僅僅在用戶觸發(fā) scrolling 的時(shí)候使用LAYER_TYPE_HARDWARE,在其他時(shí)候,使用 LAYER_TYPE_NONE。


不要給自己的人生設(shè)限
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,765評(píng)論 25 709
  • 原文地址:http://www.android100.org/html/201606/06/241682.html...
    AFinalStone閱讀 1,278評(píng)論 0 1
  • 故事太早 年輕的心 還不能一一整理開(kāi)場(chǎng)的序言 順流而來(lái)的風(fēng) 拂過(guò)臉頰 這一瞬 是給 故事的開(kāi)場(chǎng) 是給過(guò)往 云煙里自...
    凡夢(mèng)之愿閱讀 213評(píng)論 0 0
  • 一整天的陰雨,夜晚的空氣又沉又涼,心情壓抑極了,抽支煙發(fā)暈,路邊姑娘跟男友無(wú)言的擁抱在路燈下,總覺(jué)著一切都似曾相識(shí)...
    dryas閱讀 306評(píng)論 0 0
  • 看完星爺和徐老怪的作品《西游2——伏妖篇》后,出于一名教師的職業(yè)本能,我首先想到的居然是孩子的教育問(wèn)題。 先說(shuō)一下...
    蝸牛十年閱讀 971評(píng)論 2 6

友情鏈接更多精彩內(nèi)容