自定義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. 實現(xiàn)自己的events
4. 兼容Android各平臺
1.1 繼承一個 View
所有framework中提供的View類都繼承自View熊杨。我們的自定義View可以直接繼承View上炎,也可以繼承View的子類荡澎,比如Button,TextView
必須實現(xiàn)下面的構造函數(shù)
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名字相同局装,不過這不是強制的坛吁,但是正常開發(fā)中一般都這么做劳殖。
完成上面的xml后,我們就可以在layout中給這些屬性賦值拨脉。就像Android提供的原生屬性一樣哆姻,唯一不同的是我們的自定義屬性屬于另外一個namespace.
http://schemas.android.com/apk/res/com.gome.farmpatner
默認的命名空間是
http://schemas.android.com/apk/res/android
根據(jù)上面的例子,在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中創(chuàng)建之后劳景,所有的屬性值都會從resource bundle中讀出來并存儲到一個AttributeSet中,這個AttributeSet最終會傳給我們view的構造函數(shù)碉就。
應該使用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初始化完成之后才能獲取的到碉熄。
一般會提供一個動態(tài)的接口讓調用者去控制桨武。也就是getter和setter。
public boolean isShowText() {
return mShowText;
}
public void setShowText(boolean showText) {
mShowText = showText;
invalidate();
requestLayout();
}
注意setter方法中锈津,最后調用了invalidata和requestLayout,這樣View才會重新繪制和布局呀酸,才能將調用者想要的效果立馬顯示到View中。
如果屬性影響到View的展示琼梆,那么我們一定得調用invalidate()來通知系統(tǒng)對View進行重繪性誉。
如果屬性值影響到view的大小或者形狀等布局類的內容,則一定要調用requestLayout來通知系統(tǒng)對View進行重新布局茎杂。
在自定義View中错览,可以根據(jù)需要暴露出一些event的相關接口,提供一個listener的interface供調用者實現(xiàn)煌往。
對于本章節(jié)倾哺,最基本的規(guī)則就是:我們應該將那些會影響到View的展示和行為的property都給暴露出來。
1.2.4 Design For Accessibility###
這主要是Google提出來刽脖,為了殘障人士準備的悼粮。一些殘疾人可能看不見或者使用不了觸摸屏的用戶。
這部分內容具體可以看看https://developer.android.com/guide/topics/ui/accessibility/apps.html#custom-views
1.3 實現(xiàn)自定義的繪制
1.3.1 Override onDraw
onDraw(Canvas canvas)
在使用canvas繪制之前曾棕,我們先得有paint對象扣猫,下面就是paint的相關介紹
1.3.2 創(chuàng)建需要繪制的對象
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);
}
提前創(chuàng)建paint對象是至關重要的優(yōu)化手段勺远,因為View會被重繪的很頻繁,如果我們每次都在onDraw中創(chuàng)建對象的話會相當影響程序性能时鸵。
1.3.3處理Layout相關事件###
為了能夠準確的繪制我們的自定義View胶逢,就得知道size是多大。
復雜一點的自定義View經常需要根據(jù)size和處于屏幕中的位置來執(zhí)行多次layout計算饰潜。我們絕不可以假設我們的view在屏幕中占多大位置初坠。即便是只有一個app使用我們的view,但是該app也得處理不同的屏幕尺寸彭雾,不同分辨率碟刺,以及橫豎屏這些不同情況下的展示。
如果沒有特別的需求薯酝,只要override onSizeChanged函數(shù)就好了半沽。
當view的size確定后,因為一些原因size發(fā)生了變化吴菠,這時候會調用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的參數(shù)嗓化,可以復寫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用來確定最終的寬高的大小
--------之前看過幾本書中,都是自己根據(jù)MeasureSpec的mode去計算谦屑,其原理跟Android提供的函數(shù)resolveSizeAndState是一樣的驳糯。
-> onMeasure沒有返回值,該方法使用setMeasuredDimension來傳遞結果氢橙,調用該方法是必須的酝枢,否則,會拋出異常悍手。
1.3.4 Draw 繪制
一旦你初始化了一些必須的object帘睦,比如paint什么的袍患,你就可以實現(xiàn)自己的onDraw函數(shù)了。
雖然每個view的繪制過程都不一樣竣付,不過都有幾個通用的接口:
drawText: 繪制textsetTypeface設置字體诡延,setColor設置顏色
drawRect、drawOval古胆、drawArc: 繪制簡單的形狀肆良,使用setStyle來設置是否填充內部,外邊線的繪制
drawPath: 繪制更為復雜的形狀逸绎,通過添加直線和曲線來創(chuàng)建一個自定義的Path對象惹恃,然后使用drawPath()繪制到view上,Path也可以使用setStyle.
setShader()&LinearGradient: 使用LinearGradient對象設置漸變填充,然后調用setShader來將LinearGradient應用到對應的shape中讲弄。
drawBitmap: 繪制bitmap
1.4 處理用戶交互
1.4.1 Input Gestures
用戶的操作會觸發(fā)相關的回調函數(shù)淡喜,并傳入相關的events,我們可以復寫這些callbacks來實現(xiàn)我們跟用戶交互的邏輯曲秉。
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。
我們需要實現(xiàn)GestureDetector.OnGestureListener接口识啦,來實現(xiàn)自己的處理邏輯负蚊。如果我們僅僅是想處理部分的手勢邏輯,那么我們可以選擇繼承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其他的處理函數(shù)都不會被調用冕茅。
下面的代碼在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 (創(chuàng)建模擬物理的動作)
gesture是一種比較強大的控制屏幕操作的方案伤极,不過比較難以記憶,除非提供出一種物理上合理的操作姨伤。比如listview用力滑動一下哨坪,然后抬手,listview還會繼續(xù)滑動一定的距離乍楚,就類似物理上的慣性当编。Android中的一個例子就是fling gesture.
Scroll類是處理fling gesture的基礎
想要開始一個fling(就是一個拋動,在屏幕上快速滑動然后抬起手指),可以調用fling徒溪,參數(shù)是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在物理上是精準的牵舱,不過實際上會發(fā)現(xiàn)這個值會讓滑動變得很快串绩,所以一般我們都會將velocityX和velocityY除以一個4或者8.
-->fling()函數(shù)幫助我們建立了fling的物理模型,然后芜壁,我們需要每隔一段時間調用Scroller.computeScrollOffset()來更新scroller礁凡。computeScrollOffset會通過物理模型計算出xy坐標的位置,然后更新scroller的內部狀態(tài)慧妄∏昱疲可以調用getCurrX()和getCurrY()獲取到對應的值。
大部分view都是直接將Scroller的xy值傳遞給scrollTo塞淹。當然也可以使用其他動畫窟蓝,比如rotate。
if (!mScroller.isFinished()) {
mScroller.computeScrollOffset();
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
}
Scroll會為我們計算出scroll的位置饱普,但是它并不會自動把這些改動apply到View上运挫。我們應該做的是確保以足夠的頻率來get和apply新的坐標到view上,這樣滾動的動畫才會平滑套耕。一般谁帕,有兩種方式來實現(xiàn):
-->在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.
每當我們有什么屬性發(fā)生變化的時候征冷,并不是直接更新到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的優(yōu)化##
要想界面流暢不丟幀踊赠,那么就要保證1秒鐘60幀左右呵扛。
為了增加View的流暢度,那么就要把一些不必要的代碼從需要頻繁調用的代碼中剝離出來筐带。
-->先從onDraw開始今穿,下面的策略會帶來很大的回報。首先應該避免在onDraw中創(chuàng)建對象伦籍,因為局部變量的allocation會頻繁喚醒GC從而有可能造成界面的卡頓蓝晒∪觯可以在初始化的時候或者多個動畫之間的時候去分配內存,但是切記不要在動畫執(zhí)行的時候去執(zhí)行allocation芝薇。
-->另外胚嘲,盡量減少onDraw不必要的調用,大部分onDraw的回調都是因為invalidate()的調用洛二,所以要減少invalidate()不必要的調用馋劈。
-->另外一個耗時操作是布局的遍歷。當我們調用requestLayout的時候晾嘶,Android需要遍歷整個view的層級去確定每個view的size妓雾。如果存在一些沖突,那么可能會多次遍歷垒迂。保證你的ViewGroup的層級盡可能的少械姻。
-->如果你要實現(xiàn)的是一個很復雜的UI,應該考慮自定一個ViewGroup机断。不同于自帶的views楷拳,自定義view可以根據(jù)應用場景對子view的大小和位置做一些預設和假定,因此會一定程度上避免多次遍歷children來layout吏奸。
比如把子view的大小和位置直接寫死唯竹,就不需要measure子view了。
參考:https://developer.android.com/training/custom-views/index.html