View的基礎知識
View是所有控件的基類,ViewGroup繼承了View, ViewGroup表示一個控件組,內部可以包含多個控件, 例如LineraLayout就是繼承的ViewGroup,它里面可以包含多個子控件。即View可以是單個控件也可以是多個控件組成的控件組。
-
View的位置參數
View在平移過程中,left、top、right、bottom這幾個值是不會改變的,改變的是x、y、translationX、
translationY這幾個值參數名 解釋 left View的左邊界距父容器左邊界的距離 top View的上邊界距父容器上邊界的距離 right View的右邊界距父容器左邊界的距離 bottom View的底邊界距父容器上邊界的距離 x left+translationX y top+translationY translationX View的左上角X方向相對于父容器的偏移量 translationY View的左上角Y方向相對于父容器的偏移量 -
MotionEvent和TouchSlop
- 典型的MotionEvent事件
名稱 解釋 ACTION_DOWN 手指剛接觸屏幕 ACTION_MOVE 手指在屏幕上滑動 ACTION_UP 手指從屏幕上松開的一瞬間 - 獲取點擊位置的坐標
getX()/getY() 獲取的是相對于當前View左上角的坐標
getRawX()/getRawY() 獲取的是相對于手機屏幕左上角的坐標 - TouchSlop
系統識別認為是滑動的最小距離/** * Distance a touch can wander before we think the user is scrolling in dips. * Note that this value defined here is only used as a fallback by legacy/misbehaving * applications that do not provide a Context for determining densit configuration-dependent * values. * * To alter this value, see the configuration resourc config_viewConfigurationTouchSlop * in frameworks/base/core/res/res/values/config.xml or the appropriate devic resource overlay. * It may be appropriate to tweak this on a device-specific basis in an overla based on * the characteristics of the touch panel and firmware. */ private static final int TOUCH_SLOP = 8;
-
VelocityTracker、 GestureDetector、 Scroller
- VelocityTracket
//獲取實例 private var velocityTracker: VelocityTracker = VelocityTracker.obtain() //添加事件 /** * Add a user's movement to the tracker. You should call this for the * initial {@link MotionEvent#ACTION_DOWN}, the following * {@link MotionEvent#ACTION_MOVE} events that you receive, and the * final {@link MotionEvent#ACTION_UP}. You can, however, call this * for whichever events you desire. * * @param event The MotionEvent you received and would like to track. */ velocityTracker.addMovement(event) //在MotionEvent#ACTION_UP的時候計算速率 /** *@param units 單位為毫秒(millisecond),表示速率的單位時間 * 如值是1000,則在1000毫秒內滑過100像素(px),速率就是100 * 如值是100,如果在100毫秒內同樣滑過100像素,速率也是100 * 速率 = 終點位置(ACTION_DOWN)- 初始位置(ACTION_DOWN)/(經過的時間/units) */ velocityTracker.computeCurrentVelocity(1000) // 獲取x、y方向上的速率 val xVelocity = velocityTracker.xVelocity val yVelocity = velocityTracker.yVelocity //將實例重置為初始狀態 velocityTracker.clear() //回收內存 velocityTracker.recycle()
- GestureDetector
Android手勢介紹 - Scroller
實現View的彈性滑動,詳情見下文
- VelocityTracket
View的滑動
實現滑動的方式及彈性滑動
- 滑動
實現滑動的方式 |
---|
scrollTo()/scrollBy() |
View動畫 |
屬性動畫 |
LayoutParams |
- scrollTo()/scrollBy()
// 只是移動View里面的內容,對于像ImageView之類的單個控件來說,內容就是里面的圖片
// 對于ViewGroup這種控件組,內容就是其中的子View.
// scrollBy()是相對目前的scrollX, scrollY進行移動
// scrollTo()則是覆蓋之前的scrollX, scrollY進行移動
// scrollX == View的左邊坐標 - View內容的左邊坐標,即scrollX為負值時,View內容向右移
// scrollY == View的頂部坐標 - View內容的頂部坐標, scrollY為負值時,View內容向下移動
SLIDE_MODE_SCROLL -> {
scrollTo(-event.x.toInt(), 0)
}
- View動畫
//移動后,點擊響應的位置還在原來的區域
// left,top,right,bottom屬性不變
// x, y屬性不變
SLIDE_MODE_ANIMATION -> {
val translateTime = (Math.abs(event.x - x) / 100 * 1000).toLong()
val translateAnimation = TranslateAnimation(x, event.x, y, y).apply {
duration = translateTime
fillAfter = true
}
this.startAnimation(translateAnimation)
}
- 屬性動畫
//移動后,點擊響應的位置在移動后的區域
// left,top,right,bottom屬性不變
// translationX,translationY改變 導致x, y屬性改變
SLIDE_MODE_ANIMATOR -> {
val translateTime = (Math.abs(event.x - x) / 100 * 100).toLong()
ObjectAnimator.ofFloat(this, "translationX", x, event.x).setDuratio(translateTime).start()
}
- LayoutParams
//移動后,點擊響應的位置在移動后的區域
// left,top,right,bottom屬性根據情況改變
// left,top改變 導致x, y屬性改變
SLIDE_MODE_LAYOUT_PARAMS -> {
(this.layoutParams ViewGroup.MarginLayoutParams)?.apply {
leftMargin = event.x.toInt()
this@SampleViewGroup.requestLayout()
}
}
- 彈性滑動
- 彈性滑動實際上是將一次滑動“微分”成一次次小滑動,并在一個合理的時間段內完成,而不像普通的滑動一次滑動就完成所有工作
- 實現彈性滑動一般結合scrollTo()來實現
實現彈性滑動的方式 |
---|
使用Scroller |
使用屬性動畫 |
延時策略(Handler#sendMessageDelay()) |
- Scroller
//初始化時創建實例
private var mScroller: Scroller = Scroller(context)
//重寫computeScroll實現
SLIDE_MODE_SMOOTH_SCROLL -> smoothScrollTo(-event.x.toInt())
private fun smoothScrollTo(destX: Int) {
val deltaX = destX - scrollX
val time = Math.abs(deltaX) / 100 * 1000
mScroller.startScroll(scrollX, y.toInt(), deltaX, y.toInt(), time)
//重繪
invalidate()
}
//會在draw的時候調用computeScroll()
override fun computeScroll() {
//計算當前的滑動偏移
//滑動未結束則返回true
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.currX, mScroller.currY)
//scrollTo()之后重繪,達成彈性滑動的效果
postInvalidate()
}
}
- 屬性動畫
//動畫實現彈性滑動
SLIDE_MODE_ANIMATOR_SCROLL -> {
lastScrollX = scrollX
mScrollX = -event.x.toInt() - scrollX
ValueAnimator.ofInt(0, 1).apply {
duration = Math.abs(mScrollX) / 100 * 1000.toLong()
addUpdateListener {
it.animatedFraction
this@SampleViewGroup.scrollTo((it.animatedFraction * mScrollX).toInt() + lastScrollX, 0)
}
start()
}
}
- 延時策略(Handler#sendMessageDelay())
//通過Handler#sendMessageDelay實現彈性滑動
SLIDE_MODE_HANDLER_SMOOTH -> {
lastScrollX = scrollX
mScrollX = -event.x.toInt() - scrollX
intervalX = if (mScrollX < 0) -10 else 10
mHandler.sendMessage(Message.obtain(nul SLIDE_MODE_HANDLER_SMOOTH, intervalX, 0 ))
}
//Handler
private class SlideHandler(private val weakReference: WeakReference<SampleViewGroup>) : Handler() {
private var scrollerX = 0
override fun handleMessage(msg: Message?) {
if (msg?.what == SLIDE_MODE_HANDLER_SMOOTH) {
weakReference.get()?.run {
scrollerX += msg.arg1
scrollTo(scrollerX + lastScrollX, 0)
mScrollX -= intervalX
if (Math.abs(mScrollX) >= Math.abs(intervalX)) {
mHandler.sendMessageDelayed(Message.obtain(null, SLIDE_MODE_HANDLER_SMOOTH, intervalX, 0), 100)
} else {
scrollerX = 0
}
}
}
}
}
滑動沖突
滑動沖突場景 | 處理原則 |
---|---|
外部滑動方向和內部滑動方向不一致 | 根據x方向滑動距離和y方向的滑動距里的差值 或 根據二者的夾角來判斷滑動方向,如ViewPager的解決方式 |
外部滑動方向和內部滑動方向一致 | 根據具體需求,如可以根據滑動的距離,滑動起始位置來判斷具體是哪個部分滑動 |
以上兩者的嵌套 |
-
滑動沖突處理方式
滑動沖突處理方式 外部攔截法,父容器需要就攔截,不需要就不攔截。重寫父容器的onInterceptTouchEvent 內部攔截法,父容器也需要配合修改,子元素需要就直接消耗,重寫dispatchTouchEvent且要配合requestDisallowTouchEvent,參考ViewPager
- 外部攔截法-簡單示例
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
var intercepted = false
when(ev?.action){
//如果攔截了ACTION_DOWN,后續的事件都會在當前View中處理
//子View中的onClick(在ACTION_UP的時候)之類的事件也不會觸發
MotionEvent.ACTION_DOWN ->{
intercepted =false
}
MotionEvent.ACTION_MOVE ->{
//是否需要事件
intercepted = needMotionEvent()
}
// 如果在這之前已經攔截了,返回true和false 相差不大
// 如果之前沒有攔截,此處返回了true,那么子View中設置的onClick(ACTION_UP的時候)就會無效
MotionEvent.ACTION_UP ->{
intercepted = false
}
}
return intercepted
}
- 內部攔截法(可參考ViewPager)-簡單示例
//子元素
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
when(event?.action){
//ACTION_DOWN在父元素中默認不攔截,如果攔截了ACTION_DOWN的話之后的事件都不會傳遞
MotionEvent.ACTION_DOWN ->{
//父元素不攔截事件
parent.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE ->{
if (parentNeedEvent()){
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
return super.dispatchTouchEvent(event)
}
//父元素
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
return ev?.action != MotionEvent.ACTION_DOWN
}
View的事件分發
Note:
1. 事件的傳遞從Activity開始到View結束,如果被消費了就不再繼續傳遞,ACTION_DOWN是事件開始的標識,不會被攔截。例如重寫了Activity的onTouchEvent方法并返回true,則ACTION_DOWN之后的事件都不會向下一級分發。
2. 事件的消費從View開始到Activity結束,只能消費一次例如一個自定義View,重寫了onTouchEvent方法并返回true,則表示要消費此次事件,這個自定義View會接收ACTION_DOWN之后的事件,而在它的消費鏈下一級(ViewGroup、 Activity)一些用來消費事件的方法不會被調用(onTouchEvent、一些點擊事件)
3. onTouchListener的執行順序在onTouchEvent之前,如果onTouchListener返回true消費了事件則onTouchEvent不會調用,一些點擊事件是在onTouchEvent中處理的。
4. 像onClick(ACTION_UP后執行),onLongClick(ACTION_DOWN一般延時500ms后還是press后執行)都是在onTouchEvent中執行的,設置了這些點擊事件則onTouchEvent返回的是true.
5. onLongClick中返回的bool值,true表示消費結束了,其它點擊事件不再響應,false則其他點擊事件還可以響應.
View的工作原理
基本概念
- ViewRoot、DecorView、Window
- 在ActivityThread#performLaunchActivity()中執行Activity#attach , Activity#attach中創建了PhoneWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
- 在ActivityThread#handleResumeActivity中調用了Activity#makeVisiable
void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); }
- 在Activity#makeVisible中調用了WindowManagerImpl#addView
- WindowManagerImpl#addView中調用了WindowManagerGlobal#addView,在WindowManagerGlobal#addView中創建了ViewRootImpl,
通過ViewRootImpl#setView將Window相關屬性和DecorView關聯了起來。
... root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams); // do this last because it fires off messages to start doing things try { root.setView(view, wparams, panelParentView); } catch (RuntimeException e) { // BadTokenException or InvalidDisplayException, clean up. if (index >= 0) { removeViewLocked(index, true); } throw e; } ...
- ActivityThread#performResumeActivity最終會執行到Activity#onResume,ActivityThread#handleResumeActivity在performResumeActivity之后。也就是說ViewRootImpl的創建在Activity#onResume回調執行之后
-
繪制流程
- 在ViewRootImpl中
final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } } final TraversalRunnable mTraversalRunnable = new TraversalRunnable(); void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } } void unscheduleTraversals() { if (mTraversalScheduled) { mTraversalScheduled = false; mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); mChoreographer.removeCallbacks( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); } } void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); if (mProfile) { Debug.startMethodTracing("ViewAncestor"); } performTraversals(); if (mProfile) { Debug.stopMethodTracing(); mProfile = false; } } }
- 在ViewRootImpl#perforTranversals中按順序調用:
測量:performMeasure -> View#measure -> View#onMeasure
布局:performLayout -> View#layout -> View#onLayout
繪制:performDraw -> View#draw -> View#onDraw - 測量中MeasureSpec的獲取
ViewRootImpl#dispatchResized(將傳進來的數據包括屏幕信息用Message包裝,用ViewRootHandler發送) ->
ViewRootHandler(mWinFrame.set((Rect) args.arg1);) ->
getRootMeasureSpec(mWinFrame.width/mWinFrame.height,LayoutParams)(根據寬高及Laoutparams屬性組裝MeasureSpec) ->
performMeasure(widhMeasureSpec,heightMeasureSpec)
NOTE:
- Android在子線程中更新UI的時候會拋出異常,這個是在ViewRootImpl#checkThread()中處理的
void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } }
ViewRootImpl的創建是在Activity#onResume之后才創建的,所以如果是在ViewRootImpl創建之前在子線程中更新UI是不會拋出異常的
原因: 因為Android的View控件是非線程安全的,所以要進行checkThread(),如果加入線程同步的話會出現兩個問題:1. 使邏輯變得復雜;2.鎖機制會降低UI訪問效率,因為在多個線程的情況下會阻塞一些線程的運行
-
MeasureSpec
- MeasureSpec是個32位的int值,高2位表示SpecMode,低30位表示SpecSize
private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; /** * Measure specification mode: The parent has not imposed any constraint * on the child. It can be whatever size it wants. */ public static final int UNSPECIFIED = 0 << MODE_SHIFT; /** * Measure specification mode: The parent has determined an exact size * for the child. The child is going to be given those bounds regardless * of how big it wants to be. */ public static final int EXACTLY = 1 << MODE_SHIFT; /** * Measure specification mode: The child can be as large as it wants up * to the specified size. */ public static final int AT_MOST = 2 << MODE_SHIFT; public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) { if (sUseBrokenMakeMeasureSpec) { // Android版本<=17時才為true return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } }
UNSPECIFIED 父容器不對View有任何限制,要多大給多大,對應于系統內部的測量狀態 EXACTLY 父容器已經測量出View所需的精確大小,這時候View的最終大小就是SpecSize所指定的值。對應于LayoutParams中的match_parent和具體數值的模式 AT_MOST 父容器指定了一個可用大小SpecSize,View的大小不能大于這個值,最終多大看具體情況,對應于LayoutParams的wrap_content
- MeasureSpec和LayoutParams的對應關系
可查看ViewGroup#measureChild
parentLayoutParams | parentSpecMode | childLayoutParams | childSpecMode | childSpecSize |
---|---|---|---|---|
match_parent | EXACTLY | match_parent | EXACTLY | availableSize |
match_parent | EXACTLY | wrap_content | AT_MOST | availableSize |
match_parent | EXACTLY | dp | EXACTLY | childSize |
wrap_parent | AT_MOST | wrap_content | AT_MOST | availableSize |
wrap_parent | AT_MOST | match_parent | AT_MOST | availableSize |
wrap_parent | AT_MOST | dp | EXACTLY | childSize |
dp | EXACTLY | match_parent | EXACTLY | availableSize |
dp | EXACTLY | dp | EXACTLY | childSize |
dp | EXACTLY | wrap_content | AT_MOST | availableSize |
UNSPECIFIED | match_parent | UNSPECIFIED | UNSPECIFIED | |
UNSPECIFIED | wrap_content | UNSPECIFIED | UNSPECIFIED | |
UNSPECIFIED | dp | EXACTLY | childSize |
1. 如果View是采用固定寬高,不管父容器是什么模式,View都是EXACTLY
2. 如果父容器是AT_MOST模式,View不是采用固定寬高,則View也是AT_MOST模式
3. 如果View是AT_MOST模式,默認情況下會鋪滿剩余的所有空間,**這樣的話就會于match_parent是一樣的效果**,所以自定義View的時候最好對AT_MOST作自定義處理.
measure
-
measure的流程
- View#onMeasure()#getDefaultSize()
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //setMeasureDimension(),設置View的測量寬高值 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); //根據測量模式,返回對應的Size switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; } //mMinWidth是設置的布局屬性“minWidth",默認是0 case R.styleable.View_minWidth: mMinWidth = a.getDimensionPixelSize(attr, 0); break; // 如果背景是null的話,取mMinWidth // 如果背景不為null的話,取mMinWidth、背景寬度中的最大值 // ShapeDrawable沒有原始寬度,BitmapDrawable有原始寬度(圖片尺寸) protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }
- ViewGroup#measureChild()、ViewGroup#onMeasure()
- ViewGroup中沒有重寫View中的onMeasure而是交給具體的ViewGroup去根據各自特性實現
- 定義了measureChild(),用于得到childView的MeasureSpec及執行childView.measure(),在具體的ViewGroup#onMeasure中調用。
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); //根據父類的MeasureSpec,Padding,子類的LayoutParams得到傳給子類的MeasureSpec final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
- 如何在View的測量執行結束后,獲取View的寬/高
由于View的measure和Activity的生命周期不是同步的,所以并不能確定在某個Activity的生命周期內,View的measure已經完成了。有以下幾種方式能確切的獲取View的寬高- 重寫Activity#onWindowFocusChanged()
onWindowFocusChanged()在窗口焦點改變的時候調用,調用這個方法時表示View已經準備好了,寬高已經能夠準確獲取。但是會調用多次(頻繁地Activity#onResume獲取焦點,Activity#onPause失去焦點)
- 重寫Activity#onWindowFocusChanged()
2. view.post(runnable)
通過View#post()將一個**Runnable**添加到消息隊列的末尾,等到 Looper調用到此Runnable時,View已經準備完畢了。
3.ViewTreeObserver#onGlobalLayoutListener
**View樹的狀態改變,此方法會被回調多次**
```kotlin
view.viewTreeObserver().addOnClobalLayoutListener{
//method body
}
```
4.view.measure(widthMeasureSpec,heightMeasureSpec)
**View的尺寸是30位二進制,故(1 << 30) -1)**
| | |
|:-------------|:-----|
| match_parent |如果是childView的話,需要知道parentView中的剩余空間,如果是parentView,則可以作為具體數值的方式處理(屏幕的寬高)|
| 具體數值(50dp)|` widthMeasureSpec = MeasureSpec.makeMeasureSpec(50, MeasureSpec.EXACTLY)` `heightMeasureSpec = MeasureSpec.makeMeasureSpec(50, MeasureSpec.EXACTLY)` `view.measure(widthMeasureSpec,heightMeasureSpec)`|
| wrap_content |` widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<< 30)-1, MeasureSpec.AT_MOST)` `heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<< 30)-1, MeasureSpec.AT_MOST)` `view.measure(widthMeasureSpec,heightMeasureSpec)`|
layout
1. layout()中**setFrame**確定元素四個頂點的位置,調用onLayout確定子元素的位置
2. 測量寬高默認情況下等于最終的寬高,但有些特殊情況
```java
//重寫layout方法,改變了四個頂點的值
public void layout(int l, int t, int r, int b){
super.layout(l+50, t, r, b);
}
```
**還有就是多次measure的情況,在前幾次的measure中測量寬高可能和最終寬高不同**
draw
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
當確切知道需要繪制內容時,關閉WILL_NOT_DRAW。
ViewGroup中默認啟用WILL_NOT_DRAW,View中默認關閉WILL_NOT_DRAW。
- 繪制背景
// Step 1, draw the background, if needed if (!dirtyOpaque) { drawBackground(canvas); }
- 繪制自身內容
// Step 3, draw the content if (!dirtyOpaque) onDraw(canvas);
- 繪制children
// Step 4, draw the children dispatchDraw(canvas);
- 繪制裝飾
// Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas);