Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Google教我如何定制自己的View

Google教我如何定制自己的View

編輯:關於Android編程

前言

今天我看了Google教程中有關定制View的相關內容,這是之前從來沒有接觸過的領域,在github上能經常看到一些大神自定義的View,比如按鈕,ListView,好像他們天生就可以隨心所欲的定制自己的View,而自己也不知道如何入門,今天再Google上碰到了這一節,就心血來潮的看了看,Google只講了大概,看完還不是很懂,不過也學到一些東西。這一塊的內容還是在理解的基礎上多看看代碼才能明白呀。


正文

在項目裡,我們經常會遇到一些需求是Android內置View無法滿足我們的,這時我們就需要定義自己的View。

創建一個View類

一個設計良好的類包含很多容易使用的功能,而且也能有效的利用CPU和內容資源,它還應該具有以下的特性:

符合Android標准 提供XML可指定的自定義屬性 發送事件 兼容多種Android平台

Android提供了基本一些基本類和XML標簽能夠幫助我們指定符合這些要求的類。

定義一個View子類

Android裡面所有的View類全部都繼承於View類,所以你也應該去繼承View類,當然也可以為了節約時間去繼承已有的View類,比如說Button。

為了讓Android Studio去和你的View交互,你至少應該提供一個構造函數,把Context和AttributeSet對象作為參數,這個構造函數能夠允許讓布局編輯器去創建和編輯你的View實例。

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

定義自定義的屬性

為了把一個內置的View添加到UI,你應該通過XML元素的方式制定它並且通過元素屬性控制它的外觀和行為。定義良好的View可以通過XML的方式添加和制定樣式。為了在View中做到這一點,你應該:

通過一個< declare-styleable >資源元素去指定自定義View的屬性 確定XML布局文件裡的屬性值 在運行時重新獲取這些屬性值 把這些屬性值應用給你的View

這節討論如何去定制這些屬性和確定他們的值,下一節會講如何重新獲取並在運行時應用他們。

為了定義一個自定義的屬性,添加< declare-styleable >元素到你的項目裡。習慣上一般都把它放到res/values/attrs.xml文件裡,這裡有一份XML文件的樣例:


   
       
       
           
           
       
   

這份代碼聲明了兩個定制的屬性,showText和labelPosition,屬於一個名叫PieChart的自定義的實體。按照慣例,這個名字應該和自定義View的類名保持一致。但也不是必須要嚴格的遵循這個管理,許多流行的代碼編輯器都依靠它自己的命名慣例去命名。

一旦你定義好了一個定制屬性,你就可以在xml文件裡面像內置屬性一樣使用它們了。唯一的不同就是這些內置屬性屬於另一個不同的命名空間。不是我之前經常見的http://schemas.android.com/apk/res/android命名空間了,它們屬於http://schemas.android.com/apk/res/[你的包名]。舉一個例子,如何應用PieChart的屬性:



 

為了避免去重復很長的命名空間URI,這個例子使用了xmlns指令。這個指令會給http://schemas.android.com/apk/res/com.example.customviews分配一個別名。你可以任意的去為你的命名空間選擇你想要的別名。

注意一下把自定義View添加到布局文件裡的XML標簽。它應該是自定義View類的完全限定名。如果你的View類是一個內部類,你就要用外部類來進一步的限定它。舉一個例子,如果PieChart由一個內部類叫做PieView。為了去使用這個類裡面的自定義屬性,你應該使用這個標簽——com.example.customviews.charting.PieChart$PieView。

應用自定義屬性

當一個view從XL布局文件裡面創建,它標簽下面所有的屬性都可以從resource bundle裡面讀取並傳遞給view的構造函數作為一個AttributeSet。雖然我們可以直接從AttributeSet裡面直接讀取,但是這麼做有一些缺點:

帶有屬性值的資源引用不能解決 樣式不能得到應用

如果我們把AttributeSet傳遞給obtainStyledAttributes()方法。這個方法會回傳一個TypedAttay類型的數組,它們的值已經被重新引用過並樣式修改過

Android的編譯器為我們使用obtainStyledAttributes()做了很多工作,對於每一個< declare-styleable >資源文件,產生的R文件定義了一個數組作為屬性的ID,也定義了一個數組為每一個屬性定義它們的標號,通過常量集合的形式。你可以通過提前定制好的常量去從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是一個可共享的資源,我們用完之後應該及時回收。

添加屬性和事件

屬性是一個控制view外觀和行為的一種有效的方式,但是他們只應該在view初始化的時候讀取。為了提供動態的行為,我們應該給每一個自定義屬性提供getter和setter方法對。下面的片段展示了PieChart如何暴露一個叫showText的屬性:

public boolean isShowText() {
   return mShowText;
}

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

注意到setShowText調用了incalidate()和requestLayout()。這些調用對於確保view行為的可靠是很重要的。在可能會影響view的屬性發生改變之後,你應該讓這個view無效,這樣系統才知道這個view需要重繪。比如,如果一個屬性的改變可能會應該這個view的大小和形狀,你應該去請求一個新的布局。忘記這些方法的調用會造成很難發現的bug。

自定義View該應該支持事件監聽器以便和重要事件進行交流。比如說,PieChart暴露了一個自定義的事件叫做OnCurrentItemChanged去提醒監聽器,用戶旋轉了餅圖去注意餅圖的另一個部分。

當我們是這個自定義View的唯一使用者時,我們很容易就忘記去暴露這些屬性和事件。花一些實踐去仔細的確定view的接口可以降低未來的維護成本。一個值得我們去遵循的好的規則是,總是去曝光那些會影響View外觀和行為的屬性。

為可使用設計

Your custom view should support the widest range of users. This includes users with disabilities that prevent them from seeing or using a touchscreen. To support users with disabilities, you should:

你自定義的View應該要支持大范圍的用戶。包括缺乏看或是使用觸屏設備的用戶,為了支持這些用戶,我們應該:

Label your input fields using the android:contentDescription attribute Send accessibility events by calling sendAccessibilityEvent() when appropriate. Support alternate controllers, such as D-pad and trackball

這三段不太懂,大家自行翻譯吧。


自已畫圖

自定義View最重要的部分就是它的外觀了。自定義畫圖可以簡單也可以很復雜,根據你的項目需求不同而不同。

覆寫 onDraw()方法

覆寫onDraw()方法是畫自定義View的一個最重要的步驟。傳遞給onDraw(0的參數是一個Canvas對象,view可以用它去畫它自己,這個Canvas類定義很多方法,能畫線,畫字,畫圖以及很多主要的圖形。你在onDraw()方法裡用這些方法創建你的自定義UI。

在你調用任何方法之間,創建一個畫筆對象是很重要的。下面會討論。

創建用於繪畫的對象

android.graphics框架把繪圖分為兩類:

畫什麼,由Canvas解決 怎麼畫,用Paint解決

舉一個例子,Canvas提供一個畫線的方法,而Paint提供了定義線的顏色的方法。Canvas提供了畫長方形的方法,而Paint可以決定是否要填充這個長方形或者就讓它空著。簡而言之,Canvas定義你想畫在屏幕上的形狀,而Paint定義了顏色,樣式,字體和很多你想要畫的形狀的方方面面。

所以在你畫任何東西之前,你應該創建一個或多個Paint對象。這個PieChart的例子中通過Init()函數完成這一操作,並在構造函數裡調用init()方法。

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

   ...

提前創建好對象是一個很重要的優化。View會被經常性的重繪,所以很多用於畫圖的對象需要昂貴的開銷。把創建繪圖對象放在onDraw()方法裡會明顯降低性能,並且會使你的UI變的拖沓。

處理布局事件

為了恰當的去重繪你的自定義View,你應該需要知道它的尺寸有多大。復雜的自定義View經常需要依據它們在屏幕上的大小和形狀進行多次布局計算。你應該永遠不要做出你屏幕上的view尺寸大小的猜想。即使只有一個APP使用你的自定義View,這個APP也需要處理不同的屏幕大,多樣的屏幕密度和在橫向與縱向模式下不同的比例。

雖然View有很多方法都可以用來處理測量,但是它們中的大多數方法都不需要被覆寫。如果你的View不需要特別控制它的大小,你只需要覆寫一個方法——onSizeChanged()。

onSizeChanged()方法在你的View第一次被分配一個大小時會被調用,如果因為任何原因,你的View大小改變,這個方法還會再次被調用。在onSizeChanged()方法裡計算位置,計算好其他任何你的View大小相關的值,而不是在重繪的時候再取計算。在PieChart的例子裡,當計算邊界角度和文本或是其他可視性元素的位置時onSizeChanged()方法會被調用。

當你為你的View分配一個尺寸時,布局管理器會假設這個尺寸包含了View的所有padding。你必須處理這些padding值當你計算View尺寸的時候。這裡由一個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的布局參數,就執行onMeasure()方法。這個方法的參數是View.MeasureSpec,這個指揮告訴你父View希望你的View是多大和是否這個大小是一個硬編碼的最大值還只是一個建議值。作為優化,這些值應該被儲存在打包過的整數裡,你應該使用View.MeasureSpec裡面的靜態方法去解壓這些儲存在每個數字裡的信息。

這裡有一個onMeasure()執行的例子。在這個執行裡,PieChart會讓這個區域盡可能大使得這個pie和它的標簽一樣大:

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

在這份代碼裡有三個地方需要注意:

計算尺寸應該考慮view的padding,如同之前提到過的,這是這個view的責任。 resolveSizeAndState()這個幫助方法是用來創建常量的寬度值和高度值。通過比較view希望得到的大小和傳遞給onMeasure()方法的spec,這個幫助方法會返回一個合適的View.MeasureSpec值。 onMeasure()方法沒有返回的值,它的結果是通過setMeasureDimension()來傳遞。必須強制的調用這個方法。如果你忽略了這個方法的調用,View類就會拋出runtime異常。

畫圖!

一旦你完成了繪畫對象的創建並測量好了View的代碼,你就可以在onDraw()方法裡面執行它。每個View的onDraw()方法都各不相同,但是它們之間有一些常用的操作:

用drawText()畫字,用setTypeface()確定字形,用setColor()確定字的顏色 用drawRect(),drawOval(),drawAcr()來畫一些原始的形狀。用setStyle()去決定這些形狀是否被填充,描繪外邊框。 用Path類來畫更加復雜的形狀。通過給path類添加直線和曲線來定義一個形狀,然後用drawPath()來畫出來。如果只是一些簡單的圖形,可以用setStyle()確定path是否描繪外邊框是否被填充。 創建LinearGradient對象來定義漸變填充。用你的LinearGradient調用setShader()方法去填充形狀。 使用drawBitmap()去畫bitmap

下面是畫piechart的代碼,展示如何畫字,線和形狀:

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只是創建自定義View的一部分,你也需要像在真實世界裡那樣讓你的View相應用戶的輸入。對象影響表現的像真實世界裡面的物體一樣。比如說,圖片不能突然出現或是消失,因為真實世界裡不會那樣,圖片應該從一個地方移動到另一個地方。

用戶也會察覺到這種微妙的行為,最好表現的和真實世界裡面一樣。舉一個例子,當用戶移動一個UI對象,他們應該能在物體開始運動的時候感覺到摩擦力,在手指脫離後,物體還有繼續向前滑動的沖力。

處理輸入手勢

和很多UI框架一樣,Android也支持一個輸入事件的模型。用戶的行為會轉化為一些事件,這些事件能夠觸發回調方法,你可以覆寫這些回調方法去定制你的應用如何相應用戶輸入。Android系統裡最常用的輸入就是接觸,它會觸發onTouchEvent(android.view.MotionEvent)方法。覆寫這個方法就能處理這個事件:

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

接觸事件本身並不是很實用。現代的接觸UI定義大多考慮了這些手勢,輕碰,拉,推,快速滑動和縮放。為了把這些原始的接觸事件轉化為手勢,Android也提供了GestureDetector。

構造一個GestureDetector需要傳遞一個繼承了GestureDetector.OnGestureListener的類實例。如果你只是想要處理一些簡單的手勢,你可以基層GestureDetector.SimpleOnGestureListener,而不是繼承GestureDetector.OnGestureListener接口。舉一個例子,這個代碼創建了一個類繼承GestureDetector.SimpleOnGestureListener並且覆寫了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,你總是要實現onDown()方法並返回true。這一步是必須的,定位所有的手勢都是從onDown()方法開始。如果你在onDown()方法裡面返回false,系統會認為你想要忽略接下來所有其他的手勢,在GestureDetector.OnGestureListener裡面的方法永遠不會被調用。只有你真的想忽略接下來所有的手勢時,你才能返回false。一旦你實現了GestureDetector.OnGestureListener接口,並且創建了一個GestureDetector實例,你就可以用GestureDetector去翻譯你在onTouchEvent()方法裡接收到的事件了。

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

如果你傳遞給onTouchEvent()方法的接觸事件不認為這是手勢的一部分的話,它就會返回false,你接下來就可以運行自己自定義的手勢檢測代碼了。

創建看起來正確的運動

手勢在控制觸屏設備是一個很重要的方式,但是他們也可以是意料之外的,很難去記住,除非它們會產生看起來正確的結果。一個很好的例子就是快速滑動手勢,當用戶從屏幕的一側滑動滑動手指然後抬起她,這個手勢應該要達到這樣的效果——檢測到快速滑動以後UI會馬上相應並快速滑動,然後慢下來,就像用戶在推動一個飛輪,並使它旋轉。

然而模擬這樣的效果並不是瑣碎的,許多物理和數學都需要去完成這個任務。幸運的是,Android提供了幫助類去模擬這個效果和一些其他的行為。Scroller類就是處理飛輪滑動效果的基礎。

為了開始一個滑動,我們應該調用fling()方法,傳入一個速度,最大的x,y值和最小的x,y值。對於速度的值,你也可以用在GestureDetector裡面計算好的值。

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

注意:GesturDetector計算的速度值是很准確的,但是許多開發者會認為這個值會讓滑動太快,所以一般都除以4~8的一個因子。

fling()方法的調用建立起可滑動手勢的物理模型。然後你需要在一個固定區間通過調用Scroller.computeScrollOffset()方法更新Scroller。computeScrollOffset()通過讀取當前時間,並用物理模型去計算那個時間的x,y的位置,去更新Scroller對象的狀態。調用getCurrX()和getCurrY()去獲取這些值。

大多數View直接把Scroller對象的x,y位置直接傳遞給scrollTo()。這個piechart有些不同:它用當前的y去設置需要旋轉的角度。

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

Scroller類能幫你計算好位置,但是它不會自動幫你把位置應用到你的view上。你需要確保你獲取並應用新坐標的頻率足夠大來確保你的滾動看起來足夠平滑。這裡有兩種方法:

調用postInvalidate()方法在fling()之後,去強制重繪這個技術需要你在OnDraw()方法裡面計算好偏移量,每次當偏移量改變時調用postInvalidate()。 建立一個ValueAnimator給fling計算過渡動畫,調用addUpdateListener()去添加一個監聽器處理動畫更新。

PieChart使用的第二種方法,這個方法更復雜,但是和動畫系統能更好工作,不會造成潛在不必要的無效view。這個方法在API11之前是沒有的,所以我們應該在運行時確定一下系統的版本。

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

讓你的過渡平滑

用戶期望的是在各狀態之間的過渡是平滑的。UI元素會隱隱出現,慢慢消失,而不是突然出現,突然消失。運動的開始和結束應該是平滑的,而不是突然開始,突然停止。Android的動畫框架在Android3.0被引入,使得產生平滑的過渡。

為了去使用動畫系統,何時一個屬性的改變會影響你的view的外觀,都不要直接去改變那個屬性,而是應該用ValueAnimator去做這個改變。在下面的例子裡,修改PieChart中已經選中的Pie片會導致整個餅圖開始旋轉。ValueAnimation會在數百毫秒內產生這樣一個過渡效果,而不是馬上設置一個新的旋轉值。

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

如果你想改變的基本View的屬性之一的話,做這個動畫會更加方便。因為View有內置的ViewPropertyAnimator屬性。

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

優化View

現在你有一個設計良好的View能相應用戶手勢和在狀態之間過渡,確保這個View快速運行。為了避免UI覺得拖拉,你應該確保動畫應該保持在每秒60幀。

做的少一些,調用少一些

為了加速你的View,消除一些不應要的但是會被經常調用的代碼。從onDraw()開始,這裡是回調次數最多的地方。特別的,你應該消除在onDraw()方法裡的分配,因為這個分配會成為一個垃圾收集站,讓你的view變得拖沓。把對象都放在初始化的位置,或者在過渡動畫之間,永遠不要再動畫運行的時候分配。

為了讓onDraw()更加精干,越少調用它越好。大多數對onDraw()方法的調用時調用了invalidate(),所以我們應該消除invalidate()的不必要調用。

另一個很大的開銷就是在布局文件裡消耗的時間。任何時候調用requestLayout(),Android U都必須要歷遍一遍整個View的層次結構看看每個VIEW需要多大的尺寸。如果它發現沖突,還要重復多次歷遍。UI設計師有時需要創建更深的嵌套結構去讓UI表現的更好。這些深度的View結構會造成性能問題。讓你的View層次結構越淺越好。

如果你有一個復雜的UI,考慮寫一個自定義的ViewGroup去當它的布局。和內置的View不同,你自定義的View能,你自定義的View會對自己裡面的子元素的大小和形狀作出假設,從而避免了深度歷遍的麻煩。這個PieChart展示了如何拓展ViewGroup作為自定義View的一部分,但是它從來沒有測量它。而是它會根據自己自定義的布局算法去直接設置好他們的尺寸。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved