Android View事件機(jī)制 21問21答
1.View的坐標(biāo)參數(shù) 主要有哪些?分別有什么注意的要點(diǎn)?
答:Left,Right,top,Bottom 注意這4個(gè)值其實(shí)就是 view 和 他的父控件的 相對坐標(biāo)值。 并非是距離屏幕左上角的絕對值,這點(diǎn)要注意。
此外,X和Y 其實(shí)也是相對于父控件的坐標(biāo)值。 TranslationX,TranslationY 這2個(gè)值 默認(rèn)都為0,是相對于父控件的左上角的偏移量。
換算關(guān)系:
x=left+tranX,y=top+tranY.
很多人不理解,為什么事這樣,其實(shí)就是View 如果有移動(dòng)的話,比如平移這種,你們就要注意了,top和left 這種值 是不會(huì)變化的。
無論你把view怎么拖動(dòng),但是 x,y,tranX,tranY 的值是隨著拖動(dòng)平移 而變化的。想明白這點(diǎn) 就行了。
2.onTouchEvent和GestureDetector 在什么時(shí)候用哪個(gè)比較好?
答:只有滑動(dòng)需求的時(shí)候 就用前者,如果有雙擊等這種行為的時(shí)候 就用后者。
3.Scroller 用來解決什么問題?
答:view的scrollTo和scrollBy 滑動(dòng)效果太差了,是瞬間完成。而scroller可以配合view的computeScroll 來完成 漸變的滑動(dòng)效果。體驗(yàn)更好。
4.ScrollTo和ScrollBy 有什么需要注意的?
答:前者是絕對滑動(dòng),后者是相對滑動(dòng)?;瑒?dòng)的是view的內(nèi)容 而不是view本身。這很重要。比如textview 調(diào)用這2個(gè)方法 滑動(dòng)的就是顯示出來的字的內(nèi)容。
一般而言 我們用scrollBy會(huì)比較多一些。傳值的話 其實(shí) 記住幾個(gè)法則就可以了。 右-左 x為正 否則x為負(fù) 上-下 y為負(fù),否則y為正。
可以稍微看一下 這2個(gè)的源碼:
public void scrollTo(int x, int y) { if (mScrollX != x || mScrollY != y) { int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; mScrollY = y; invalidateParentCaches(); onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { postInvalidateOnAnimation(); } } } public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); }
看到里面有2個(gè)變量 mScrollX 和mScrollY 這2個(gè)東西沒,這2個(gè)單位的 值是像素,前者代表 view的左邊緣和view內(nèi)容左邊緣的距離。 后者代表 view上邊緣和view內(nèi)容上邊緣的距離。
5.使用動(dòng)畫來實(shí)現(xiàn)view的滑動(dòng) 有什么后果?
答:實(shí)際上view動(dòng)畫 是對view的表面ui 也就是給用戶呈現(xiàn)出的視覺效果 來做的移動(dòng),動(dòng)畫本身并不能移動(dòng)view的真正位置。屬性動(dòng)畫除外。動(dòng)畫播放結(jié)束以后,view最終還是會(huì)回到自己的位置的,。當(dāng)然了你可以設(shè)置fillafter 屬性 來讓動(dòng)畫播放結(jié)束以后 view表象停留在 變化以后的位置。所以這會(huì)帶來一個(gè)很嚴(yán)重的后果。比如你的button在屏幕的左邊,你現(xiàn)在用個(gè)動(dòng)畫 并且設(shè)置了fillafter屬性讓他去了右邊。你會(huì)發(fā)現(xiàn) 點(diǎn)擊右邊的button 沒有click事件觸發(fā),但是點(diǎn)擊左邊的 卻可以觸發(fā),原因就是右邊的button 只是view的表象,真正的button 還在左邊沒有動(dòng)過。你一定要這么做的話 可以提前在右邊button移動(dòng)后的位置放一個(gè)新的button,當(dāng)你動(dòng)畫執(zhí)行結(jié)束以后 把右邊的enable 左邊的讓他gone就可以了。
這么做就可以規(guī)避上述問題。
6.讓view滑動(dòng)總共有幾種方式,分別要注意什么?都適用于那些場景?
答:總共有三種:
a:scrollto,scrollby。這種是最簡單的,但是只能滑動(dòng)view的內(nèi)容 不可以滑動(dòng)view本身。
b:動(dòng)畫。動(dòng)畫可以滑動(dòng)view內(nèi)容,但是注意非屬性動(dòng)畫 就如我們問題5說的內(nèi)容 會(huì)影響到交互,使用的時(shí)候要多注意。不過多數(shù)復(fù)雜的滑動(dòng)效果都是屬性動(dòng)畫來完成的,屬于大殺器級別、
c:改變布局參數(shù)。這種最好理解了,無非是動(dòng)態(tài)的通過java代碼來修改 margin等view的參數(shù)罷了。不過用的比較少。我本人不怎么用這種方法。
7.Scroller是干嘛的?原理是什么?
答:Scroller就是用于 讓view有滑動(dòng)漸變效果的。用法如下:
package com.example.administrator.motioneventtest; import android.content.Context; import android.util.AttributeSet; import android.widget.Scroller; import android.widget.TextView; /** * Created by Administrator on //. */ public class CustomTextView extends TextView{ private Scroller mScroller; public CustomTextView(Context context) { super(context); mScroller=new Scroller(context); } public CustomTextView(Context context, AttributeSet attrs) { super(context, attrs); mScroller=new Scroller(context); } public CustomTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mScroller=new Scroller(context); } //調(diào)用此方法滾動(dòng)到目標(biāo)位置 public void smoothScrollTo(int fx, int fy) { int dx = fx - mScroller.getFinalX(); int dy = fy - mScroller.getFinalY(); smoothScrollBy(dx, dy); } //調(diào)用此方法設(shè)置滾動(dòng)的相對偏移 public void smoothScrollBy(int dx, int dy) { //設(shè)置mScroller的滾動(dòng)偏移量 mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy,); invalidate();//這里必須調(diào)用invalidate()才能保證computeScroll()會(huì)被調(diào)用,否則不一定會(huì)刷新界面,看不到滾動(dòng)效果 } //使用scroller最重要不要遺漏這個(gè)方法 @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); //這個(gè)方法不要忘記調(diào)用。 postInvalidate(); } super.computeScroll(); } }
其實(shí)上述代碼 很多人應(yīng)該都能搜到。我們這里主要講一下 他的原理。
//參數(shù)很好理解 前面滑動(dòng)起始點(diǎn) 中間滑動(dòng)距離 最后一個(gè)是 漸變時(shí)間 //而且我們看到startScroll 這個(gè)方法就是設(shè)置了一下參數(shù) 并沒有什么滑動(dòng)的代碼在 //回到前面的demo能看到我們通常調(diào)用完這個(gè)方法以后 都會(huì)馬上調(diào)用invalidate()方法 public void startScroll(int startX, int startY, int dx, int dy, int duration) { mMode = SCROLL_MODE; mFinished = false; mDuration = duration; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX; mStartY = startY; mFinalX = startX + dx; mFinalY = startY + dy; mDeltaX = dx; mDeltaY = dy; mDurationReciprocal = .f / (float) mDuration; } //我們都知道invalidate 會(huì)觸發(fā)view的 draw方法 //我們跟進(jìn)去看 會(huì)發(fā)現(xiàn)draw方法里 會(huì)調(diào)用下面的代碼: //也就是說會(huì)調(diào)用 computeScroll方法 而view本身這個(gè)方法 //是空的所以會(huì)留給我們自己實(shí)現(xiàn) int sx = ; int sy = ; if (!drawingWithRenderNode) { computeScroll(); sx = mScrollX; sy = mScrollY; } //然后回到我們的customtextview 可以看到我們實(shí)現(xiàn)的 computeScroll方法如下: //你看在這個(gè)方法里 我們調(diào)用了scrollTo方法 來實(shí)現(xiàn)滑動(dòng),滑動(dòng)結(jié)束以后再次觸發(fā)view的重繪 //然后又會(huì)再次觸發(fā)computeScroll 實(shí)現(xiàn)一個(gè)循環(huán)。 public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); //這個(gè)方法不要忘記調(diào)用。 postInvalidate(); } super.computeScroll(); } //返回true就代表滑動(dòng)還沒結(jié)束 false就是結(jié)束了 //其實(shí)這個(gè)方法 就跟屬性動(dòng)畫里的插值器一樣 你在使用startScroll方法的時(shí)候 會(huì)傳一個(gè)事件的值, //這個(gè)方法就是根據(jù)這個(gè)事件的值來計(jì)算你每一次scrollx和scrolly的值 public boolean computeScrollOffset() { if (mFinished) { return false; } int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); if (timePassed < mDuration) { switch (mMode) { case SCROLL_MODE: final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); mCurrX = mStartX + Math.round(x * mDeltaX); mCurrY = mStartY + Math.round(x * mDeltaY); break; case FLING_MODE: final float t = (float) timePassed / mDuration; final int index = (int) (NB_SAMPLES * t); float distanceCoef = .f; float velocityCoef = .f; if (index < NB_SAMPLES) { final float t_inf = (float) index / NB_SAMPLES; final float t_sup = (float) (index + ) / NB_SAMPLES; final float d_inf = SPLINE_POSITION[index]; final float d_sup = SPLINE_POSITION[index + ]; velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); distanceCoef = d_inf + (t - t_inf) * velocityCoef; } mCurrVelocity = velocityCoef * mDistance / mDuration * .f; mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX)); // Pin to mMinX <= mCurrX <= mMaxX mCurrX = Math.min(mCurrX, mMaxX); mCurrX = Math.max(mCurrX, mMinX); mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY)); // Pin to mMinY <= mCurrY <= mMaxY mCurrY = Math.min(mCurrY, mMaxY); mCurrY = Math.max(mCurrY, mMinY); if (mCurrX == mFinalX && mCurrY == mFinalY) { mFinished = true; } break; } } else { mCurrX = mFinalX; mCurrY = mFinalY; mFinished = true; } return true; }
8.view的滑動(dòng)漸變效果總共有幾種方法?
答:三種,第一種是scroller 也是使用最多的。問題7里有解釋。還有一種就是動(dòng)畫,動(dòng)畫我就不多說了,不屬于本文范疇。最后一種也是我們經(jīng)常使用的就是用handler ,每隔一個(gè)時(shí)間間隔 來更新view的狀態(tài)。
代碼不寫了很簡單。 自行體會(huì)。
9.view的事件傳遞機(jī)制 如何用偽代碼來表示?
答:
/** * 對于一個(gè)root viewgroup來說,如果接受了一個(gè)點(diǎn)擊事件,那么首先會(huì)調(diào)用他的dispatchTouchEvent方法。 * 如果這個(gè)viewgroup的onInterceptTouchEvent 返回true,那就代表要攔截這個(gè)事件。接下來這個(gè)事件就 * 給viewgroup自己處理了,從而viewgroup的onTouchEvent方法就會(huì)被調(diào)用。如果如果這個(gè)viewgroup的onInterceptTouchEvent * 返回false就代表我不攔截這個(gè)事件,然后就把這個(gè)事件傳遞給自己的子元素,然后子元素的dispatchTouchEvent * 就會(huì)被調(diào)用,就是這樣一個(gè)循環(huán)直到 事件被處理。 * */ public boolean dispatchTouchEvent(MotionEvent ev) { boolean consume=false; if (onInterceptTouchEvent(ev)) { consume=onTouchEvent(ev); }else { consume=child.dispatchTouchEvent(ev); } return consume; }
10.view的onTouchEvent,OnClickListerner和OnTouchListener的onTouch方法 三者優(yōu)先級如何?
答:onTouchListener優(yōu)先級最高,如果onTouch方法返回 false ,那onTouchEvent就被調(diào)用了,返回true 就不會(huì)被調(diào)用。至于onClick 優(yōu)先級最低。
11.點(diǎn)擊事件的傳遞順序如何?
答:Activity-Window-View。從上到下依次傳遞,當(dāng)然了如果你最低的那個(gè)view onTouchEvent返回false 那就說明他不想處理 那就再往上拋,都不處理的話
最終就還是讓Activity自己處理了。舉個(gè)例子,pm下發(fā)一個(gè)任務(wù)給leader,leader自己不做 給架構(gòu)師a,小a也不做 給程序員b,b如果做了那就結(jié)束了這個(gè)任務(wù)。
b如果發(fā)現(xiàn)自己搞不定,那就找a做,a要是也搞不定 就會(huì)不斷向上發(fā)起請求,最終可能還是pm做。
//activity的dispatchTouchEvent 方法 一開始就是交給window去處理的 //win的superDispatchTouchEvent 返回true 那就直接結(jié)束了 這個(gè)函數(shù)了。返回false就意味 //這事件沒人處理,最終還是給activity的onTouchEvent 自己處理 這里的getwindow 其實(shí)就是phonewindow public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); } //來看phonewindow的這個(gè)函數(shù) 直接把事件傳遞給了mDecor @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } //devorview就是 我們的rootview了 就是那個(gè)framelayout 我們的setContentView里面?zhèn)鬟f的那個(gè)layout //就是這個(gè)decorview的 子view了 @Override public final View getDecorView() { if (mDecor == null) { installDecor(); } return mDecor; }
12.事件分為幾個(gè)步驟?
答:down事件開頭,up事件結(jié)尾,中間可能會(huì)有數(shù)目不定的move事件。
13.ViewGroup如何對點(diǎn)擊事件分發(fā)?
答:
viewgroup就是在actionMasked == MotionEvent.ACTION_DOWN 和 mFirstTouchTarget != null 這兩種情況來判斷是否會(huì)進(jìn)入攔截事件的流程看代碼可以知道 如果是ACTION_DOWN事件 那就肯定進(jìn)入 是否要攔截事件的流程如果不是ACTION_DOWN事件 那就要看mFirstTouchTarget != null 這個(gè)條件是否成立這個(gè)地方有點(diǎn)繞但是也好理解,其實(shí)就是 對于一個(gè)事件序列來說 down是事件的開頭 所以肯定進(jìn)入了這個(gè)事件是否攔截的流程 也就是if 括號內(nèi)。
mFirstTouchTarget其實(shí)是一個(gè)單鏈表結(jié)構(gòu)他指向的是 成功處理事件的子元素。
也就是說 如果有子元素成功處理了 事件,那這個(gè)值就不為NULL。反過來說只要viewgroup攔截了事件,mFirstTouchTarget就不為NULL,所以括號內(nèi)就不會(huì)執(zhí)行,也就側(cè)面說明了一個(gè)結(jié)論:
某個(gè)view 一旦決定攔截事件,那么這個(gè)事件所屬的事件序列 都只能由他來執(zhí)行。并且onInterceptTouchEvent 這個(gè)方法不會(huì)被調(diào)用了
final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != ; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; }
14.:果某個(gè)view 處理事件的時(shí)候 沒有消耗down事件 會(huì)有什么結(jié)果?
答:假如一個(gè)view,在down事件來的時(shí)候 他的onTouchEvent返回false, 那么這個(gè)down事件 所屬的事件序列 就是他后續(xù)的move 和up 都不會(huì)給他處理了,全部都給他的父view處理。
15.如果view 不消耗move或者up事件 會(huì)有什么結(jié)果?
答:那這個(gè)事件所屬的事件序列就消失了,父view也不會(huì)處理的,最終都給activity 去處理了。
16.ViewGroup 默認(rèn)攔截事件嗎?
答:默認(rèn)不攔截任何事件,onInterceptTouchEvent返回的是false。
17.一旦有事件傳遞給view,view的onTouchEvent一定會(huì)被調(diào)用嗎?
答:是的,因?yàn)関iew 本身沒有onInterceptTouchEvent方法,所以只要事件來到view這里 就一定會(huì)走onTouchEvent方法。
并且默認(rèn)都是消耗掉,返回true的。除非這個(gè)view是不可點(diǎn)擊的,所謂不可點(diǎn)擊就是clickable和longgclikable同時(shí)為fale
Button的clickable就是true 但是textview是false。
18.enable是否影響view的onTouchEvent返回值?
答:不影響,只要clickable和longClickable有一個(gè)為真,那么onTouchEvent就返回true。
19.requestDisallowInterceptTouchEvent 可以在子元素中干擾父元素的事件分發(fā)嗎?如果可以,是全部都可以干擾嗎?
答:肯定可以,但是down事件干擾不了。
20.dispatchTouchEvent每次都會(huì)被調(diào)用嗎?
答:是的,onInterceptTouchEvent則不會(huì)。
21.滑動(dòng)沖突問題如何解決 思路是什么?
答。要解決滑動(dòng)沖突 其實(shí)最主要的就是有一個(gè)核心思想。你到底想在一個(gè)事件序列中,讓哪個(gè)view 來響應(yīng)你的滑動(dòng)?比如 從上到下滑,是哪個(gè)view來處理這個(gè)事件,從左到右呢?
用業(yè)務(wù)需求 來想明白以后 剩下的 其實(shí)就很好做了。核心的方法 就是2個(gè) 外部攔截也就是父親攔截,另外就是內(nèi)部攔截,也就是子view攔截法。 學(xué)會(huì)這2種 基本上所有的滑動(dòng)沖突都是這2種的變種,而且核心代碼思想都一樣。
外部攔截法:思路就是重寫父容器的onInterceptTouchEvent即可。子元素一般不需要管。可以很容易理解,因?yàn)檫@和android自身的事件處理機(jī)制 邏輯是一模一樣的
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted = false; int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()) { //down事件肯定不能攔截 攔截了后面的就收不到了 case MotionEvent.ACTION_DOWN: intercepted = false; break; case MotionEvent.ACTION_MOVE: if (你的業(yè)務(wù)需求) { //如果確定攔截了 就去自己的onTouchEvent里 處理攔截之后的操作和效果 即可了 intercepted = true; } else { intercepted = false; } break; case MotionEvent.ACTION_UP: //up事件 我們一般都是返回false的 一般父容器都不會(huì)攔截他。 因?yàn)閡p是事件的最后一步。這里返回true也沒啥意義 //唯一的意義就是因?yàn)?父元素 up被攔截。導(dǎo)致子元素 收不到up事件,那子元素 就肯定沒有onClick事件觸發(fā)了,這里的 //小細(xì)節(jié) 要想明白 intercepted = false; break; default: break; } return intercepted; }
內(nèi)部攔截法:內(nèi)部攔截法稍微復(fù)雜一點(diǎn),就是事件到來的時(shí)候,父容器不管,讓子元素自己來決定是否處理。如果消耗了 就最好,沒消耗 自然就轉(zhuǎn)給父容器處理了。
子元素代碼:
@Override public boolean dispatchTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: if (如果父容器需要這個(gè)點(diǎn)擊事件) { getParent().requestDisallowInterceptTouchEvent(false); }//否則的話 就交給自己本身view的onTouchEvent自動(dòng)處理了 break; case MotionEvent.ACTION_UP: break; default: break; } return super.dispatchTouchEvent(event); }
父親容器代碼也要修改一下,其實(shí)就是保證父親別攔截down:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { return false; } return true; }
相關(guān)文章
Android接入支付寶實(shí)現(xiàn)支付功能實(shí)例
這篇文章主要介紹了Android接入支付寶實(shí)現(xiàn)支付功能實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06flutter 實(shí)現(xiàn)多布局列表的示例代碼
這篇文章主要介紹了flutter 實(shí)現(xiàn)多布局列表的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-025分鐘快速實(shí)現(xiàn)Android爆炸破碎酷炫動(dòng)畫特效的示例
本篇文章主要介紹了5分鐘快速實(shí)現(xiàn)Android爆炸破碎酷炫動(dòng)效的示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-12-12Android gradle插件打印時(shí)間戳的方法詳解
這篇文章主要給大家介紹了關(guān)于Android gradle插件打印時(shí)間戳的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),對各位Android開發(fā)者們具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-09-09詳解Android studio 3+版本apk安裝失敗問題
這篇文章主要介紹了詳解Android studio 3+版本apk安裝失敗問題,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04Android實(shí)現(xiàn)界面內(nèi)嵌多種卡片視圖(ViewPager、RadioGroup)
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)界面內(nèi)嵌多種卡片視圖,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09