Android自定義View

自定義View#

基本約束##

Conform to Android standards
Provide custom styleable attributes that work with Android XML layouts
Send accessibility events
Be compatible with multiple Android platforms.
1. 符合Android標準
2. 提供一些自定義的樣式屬性,可以在layout中配置
3. 實現自己的events
4. 兼容Android各平臺

1.1 繼承一個 View

所有framework中提供的View類都繼承自View。我們的自定義View可以直接繼承View,也可以繼承View的子類,比如Button,TextView
必須實現下面的構造函數

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

1.2聲明自定義屬性##

1.2.1 在<declare-stylable>資源元素中定義我們的自定義屬性###

res/values/attrs.xml

    <resources>
       <declare-styleable name="MyView">
           <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,屬于MyView.這里的name最好跟我們自定義的View名字相同,不過這不是強制的,但是正常開發中一般都這么做。

完成上面的xml后,我們就可以在layout中給這些屬性賦值。就像Android提供的原生屬性一樣,唯一不同的是我們的自定義屬性屬于另外一個namespace.

http://schemas.android.com/apk/res/com.gome.farmpatner

默認的命名空間是

http://schemas.android.com/apk/res/android

根據上面的例子,在layout文件中,可以這樣定義屬性值

<?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.gome.farmpatner">
 <com.gome.farmpatner.MyView
     custom:showText="true"
     custom:labelPosition="left" />
</LinearLayout>

注意到這里,對于自定義控件我們引用的是全包名。
如果MyView是另一個類CustomizedView的內部類,那么需要這么寫:
com.gome.farmpatner.CustomizedView$MyView

1.2.2 應用自定義屬性###

當View從XML中創建之后,所有的屬性值都會從resource bundle中讀出來并存儲到一個AttributeSet中,這個AttributeSet最終會傳給我們view的構造函數。

應該使用Android提供的接口去解析AttributeSet,而不是直接讀取它,因為直接讀取有兩個缺點:

a. 屬性值的類型需要自己解決
    需要手動解決資源值的類型getAttributeResourceValue(int, int),還有資源的查找也需要自己解決
    具體的可以看http://192.168.63.218:8080/source/xref/GM025_CT_S06/frameworks/base/core/java/android/util/AttributeSet.java#20
                https://developer.android.com/reference/android/util/AttributeSet.html
b. 樣式需要自己去應用
    Android提供的接口會幫我們apply這些屬性到樣式中。

正確的使用方式是:
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.MyView,
0, 0);

   try {
       mShowText = a.getBoolean(R.styleable.showText, false);
       mTextPos = a.getInteger(R.styleable.labelPosition, 0);
   } finally {
       a.recycle();
   }
}

記得TypeArray最后需要recycle()。

1.2.3 添加屬性和事件###

Attributes可以很方便的控制view的顯示和行為,不過這些值只能在view初始化完成之后才能獲取的到。

一般會提供一個動態的接口讓調用者去控制。也就是getter和setter。
public boolean isShowText() {
return mShowText;
}

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

注意setter方法中,最后調用了invalidata和requestLayout,這樣View才會重新繪制和布局,才能將調用者想要的效果立馬顯示到View中。

如果屬性影響到View的展示,那么我們一定得調用invalidate()來通知系統對View進行重繪。

如果屬性值影響到view的大小或者形狀等布局類的內容,則一定要調用requestLayout來通知系統對View進行重新布局。

在自定義View中,可以根據需要暴露出一些event的相關接口,提供一個listener的interface供調用者實現。

對于本章節,最基本的規則就是:我們應該將那些會影響到View的展示和行為的property都給暴露出來。

1.2.4 Design For Accessibility###

這主要是Google提出來,為了殘障人士準備的。一些殘疾人可能看不見或者使用不了觸摸屏的用戶。
這部分內容具體可以看看https://developer.android.com/guide/topics/ui/accessibility/apps.html#custom-views

1.3 實現自定義的繪制

1.3.1 Override onDraw

onDraw(Canvas canvas)
在使用canvas繪制之前,我們先得有paint對象,下面就是paint的相關介紹

1.3.2 創建需要繪制的對象

android.graphics包將繪制的工作分為兩部分:

a. 畫什么, canvas

b. 怎么畫, paint

canvas決定需要繪制的形狀,而paint則定義顏色,樣式,字體等內容。

mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(mTextColor);
if (mTextHeight == 0) {
   mTextHeight = mTextPaint.getTextSize();
} else {
   mTextPaint.setTextSize(mTextHeight);
}

提前創建paint對象是至關重要的優化手段,因為View會被重繪的很頻繁,如果我們每次都在onDraw中創建對象的話會相當影響程序性能。

1.3.3處理Layout相關事件###

為了能夠準確的繪制我們的自定義View,就得知道size是多大。

復雜一點的自定義View經常需要根據size和處于屏幕中的位置來執行多次layout計算。我們絕不可以假設我們的view在屏幕中占多大位置。即便是只有一個app使用我們的view,但是該app也得處理不同的屏幕尺寸,不同分辨率,以及橫豎屏這些不同情況下的展示。

如果沒有特別的需求,只要override onSizeChanged函數就好了。



當view的size確定后,因為一些原因size發生了變化,這時候會調用onSizeChanged().在onSizeChanged()里面計算位置、尺寸以及其他任何和view的size相關的值,盡量不要在draw繪制的時候去重新計算。

一旦view的size被賦值之后,layout manager就會假定這個size是包括了padding內邊距的值。所以我們在計算view的size的時候必須處理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;

// 計算出直徑
float diameter = Math.min(ww, hh);

==》如果想要更好的控制layout的參數,可以復寫onMeasure方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

MeasureSpec將mode和value打包進一個int中了,可以用位移操作獲取對應的值,不過MeasureSpec已經提供了對應的方法。

getMode(int measureSpec)
    Extracts the mode from the supplied measure specification. 
getSize(int measureSpec)
    Extracts the size from the supplied measure specification. 

有三種模式: 英文比較好理解

AT_MOST         child can be as large as it wants up to the specified size
EXACTLY         The parent has determined an exact size for the child. 
UNSPECIFIED     The parent has not imposed any constraint on the child. 

具體看一下一個復寫該方法的例子:

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

-> minw的計算包含了padding,就跟上面onSizeChanged()提到的一樣

-> resolveSizeAndState用來確定最終的寬高的大小

--------之前看過幾本書中,都是自己根據MeasureSpec的mode去計算,其原理跟Android提供的函數resolveSizeAndState是一樣的。

-> onMeasure沒有返回值,該方法使用setMeasuredDimension來傳遞結果,調用該方法是必須的,否則,會拋出異常。

1.3.4 Draw 繪制

一旦你初始化了一些必須的object,比如paint什么的,你就可以實現自己的onDraw函數了。

雖然每個view的繪制過程都不一樣,不過都有幾個通用的接口:

drawText: 繪制textsetTypeface設置字體,setColor設置顏色
drawRect、drawOval、drawArc: 繪制簡單的形狀,使用setStyle來設置是否填充內部,外邊線的繪制
drawPath: 繪制更為復雜的形狀,通過添加直線和曲線來創建一個自定義的Path對象,然后使用drawPath()繪制到view上,Path也可以使用setStyle.
setShader()&LinearGradient: 使用LinearGradient對象設置漸變填充,然后調用setShader來將LinearGradient應用到對應的shape中。
drawBitmap: 繪制bitmap

1.4 處理用戶交互

1.4.1 Input Gestures

用戶的操作會觸發相關的回調函數,并傳入相關的events,我們可以復寫這些callbacks來實現我們跟用戶交互的邏輯。

1.4.1.1 touch events

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

MotionEvent有TOUCH_DOWN, TOUCH_MOVE, TOUCH_UP

1.4.1.2 gesture

touch event比較簡單,還有一些手勢的event,包括tapping(按壓,長按?), pulling, pushing, flinging(拋,類似listview的滑動?), and zooming(縮放). 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());

onDown的return true代表我們的gesture希望處理接下來的一系列的事件,因為不管是touch還是gesture肯定都是以一個Down的操作開始的。如果這里return false,那么mListener其他的處理函數都不會被調用。

下面的代碼在onTouchEvent中判斷gesturelistener是否需要處理該事件

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

1.4.2 Create Physically Plausible Motion (創建模擬物理的動作)

gesture是一種比較強大的控制屏幕操作的方案,不過比較難以記憶,除非提供出一種物理上合理的操作。比如listview用力滑動一下,然后抬手,listview還會繼續滑動一定的距離,就類似物理上的慣性。Android中的一個例子就是fling gesture.

Scroll類是處理fling gesture的基礎

想要開始一個fling(就是一個拋動,在屏幕上快速滑動然后抬起手指),可以調用fling,參數是starting velocity(開始的速度),最大最小的xy坐標值。velocity的值我們可以直接提供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();
}

Tips:盡管GestureDetector提供的velocity在物理上是精準的,不過實際上會發現這個值會讓滑動變得很快,所以一般我們都會將velocityX和velocityY除以一個4或者8.

-->fling()函數幫助我們建立了fling的物理模型,然后,我們需要每隔一段時間調用Scroller.computeScrollOffset()來更新scroller。computeScrollOffset會通過物理模型計算出xy坐標的位置,然后更新scroller的內部狀態。可以調用getCurrX()和getCurrY()獲取到對應的值。

大部分view都是直接將Scroller的xy值傳遞給scrollTo。當然也可以使用其他動畫,比如rotate。

if (!mScroller.isFinished()) {
    mScroller.computeScrollOffset();
    scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
}

Scroll會為我們計算出scroll的位置,但是它并不會自動把這些改動apply到View上。我們應該做的是確保以足夠的頻率來get和apply新的坐標到view上,這樣滾動的動畫才會平滑。一般,有兩種方式來實現:

-->在fling后調用postInvalidate(),強制重新繪制view,這種情況下我們就需要在onDraw中計算scroll的offset,并且每當offset改變的時候都要調用postInvalidate().

-->設置一個ValueAnimator,處理fling的過程,添加一個listener處理fling動畫的update,addUpdateListener.這種方式避免有時候view不必要的重繪。3.0之后支持

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();
           scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
       } else {
           mScrollAnimator.cancel();
           onScrollFinished();
       }
   }
});

1.4.3 平滑動畫

Android屬性動畫 property animation framework.

每當我們有什么屬性發生變化的時候,并不是直接更新到view上去,而是使用valueAnimator去操作。

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

Tips:這部分后面研究

如果我們改變的是view的基本屬性值,那么就很簡單,直接使用Android封裝好的接口:

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

1.5 View的優化##

要想界面流暢不丟幀,那么就要保證1秒鐘60幀左右。

為了增加View的流暢度,那么就要把一些不必要的代碼從需要頻繁調用的代碼中剝離出來。

-->先從onDraw開始,下面的策略會帶來很大的回報。首先應該避免在onDraw中創建對象,因為局部變量的allocation會頻繁喚醒GC從而有可能造成界面的卡頓。可以在初始化的時候或者多個動畫之間的時候去分配內存,但是切記不要在動畫執行的時候去執行allocation。

-->另外,盡量減少onDraw不必要的調用,大部分onDraw的回調都是因為invalidate()的調用,所以要減少invalidate()不必要的調用。

-->另外一個耗時操作是布局的遍歷。當我們調用requestLayout的時候,Android需要遍歷整個view的層級去確定每個view的size。如果存在一些沖突,那么可能會多次遍歷。保證你的ViewGroup的層級盡可能的少。

-->如果你要實現的是一個很復雜的UI,應該考慮自定一個ViewGroup。不同于自帶的views,自定義view可以根據應用場景對子view的大小和位置做一些預設和假定,因此會一定程度上避免多次遍歷children來layout。
比如把子view的大小和位置直接寫死,就不需要measure子view了。

參考:https://developer.android.com/training/custom-views/index.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377

推薦閱讀更多精彩內容