本文將通過三個demo來讓你深刻感受到了解View的事件分發(fā)機制之后你能做什么踪宠,能做好什么!!
首先我們裝作這些概念都理解了:(下文詳細介紹)
觸摸事件類型: 主要類型三種:
ACTION_DOWN
ACTION_MOVE
ACTION_UP
完整的事件傳遞主要包括三個階段: 事件的分發(fā)套媚,攔截和消費
分發(fā):對應dispatchTouchEvent方法。返回true表示事件被當前視圖消費磁椒,不再繼續(xù)分發(fā)
攔截:對應onInterceptTouchEvent方法。返回true表示攔截此事件玫芦,不繼續(xù)分發(fā)浆熔。(viewGroup和其子類中才擁有)
消費:對應onTouchEvent方法,返回true表示消費事件桥帆,不在向上傳遞医增。
view的事件傳遞機制:
—— 觸摸事件的傳遞流程是從dispatchTouchEvent開始的,如果我們不進行重寫(也就是返回默認的父類同名函數(shù))老虫,則事件將會依照嵌套層次從外層向內(nèi)層傳遞叶骨,到底最內(nèi)層的View時,就交給它的onTouchEvent處理祈匙,該方法如果能消費該事件忽刽,則返回true,如果處理不了,則返回false媒至,這時事件會重新向外傳遞崎苗。并由外層的onTouchEvent處理,依此類推
—— 如果事件在向內(nèi)層傳遞的過程中被我們重寫事件處理函數(shù)返回true時伞剑,則會導致整個事件提前被消費斑唬,內(nèi)層View不會收到這個事件了。
—— View控件的事件觸發(fā)順序是先執(zhí)行onTouch方法黎泣,最后才執(zhí)行onClick方法恕刘。如果onTouch方法返回true的話,則事件將不會繼續(xù)傳遞抒倚,最后也不會調(diào)用onClick方法褐着,如果onTouch返回false,則繼續(xù)向下傳遞衡便。因為button的preformClick是利用onTouchEvent實現(xiàn)的献起,假設onTouchEvent沒有被調(diào)用到,那么點擊事件就無效了镣陕。
viewGroup的事件傳遞機制:
—— 觸摸事件的傳遞順序是由Activity到ViewGroup谴餐,再由ViewGrop遞歸傳遞給它的子View。
—— ViewGroup通過onInterceptTouchEvent方法對事件進行攔截呆抑,如果該方法返回true岂嗓,則事件不會繼續(xù)傳遞給子View,如果返回false或者super.onInterceptTouchEvent鹊碍,則事件會繼續(xù)傳遞給子View厌殉。
—— 在子View對事件進行消費后,ViewGroup將接收不到任何事件侈咕。
臥槽公罕,這些概念我在別的地方也看到過呀,你這也不是就bibi一些概念嗎耀销,可是到底怎么用楼眷,用在哪里呢,這些概念表達的意思又到底是個啥呀熊尉?
咱們舉個例子哈罐柳,在一個美好的早晨,一家子人都起來啦狰住,打開門迎接美好的一天张吉,突然天上掉下一個餡餅,還是金的催植,掉在你的祖爺爺面前肮蛹。(金餡餅就是我們的事件)勺择,這時大家就聚在一起啊,你的祖爺爺(activity)輩分最大蔗崎,餡餅也是掉他那的酵幕,先擁有這個餡餅的分配權,你的祖爺爺非常愛你們缓苛,他說這餡餅啊芳撒,我都不久于人世了用不著,留給我的寶貝兒子吧(ViewGroup)未桥,他兒子不就是你爺爺嗎笔刹,你爺爺也愛你爸啊,就又給了你爸冬耿,你爸最后給了你舌菜,這時你就開心了,我拿到了這個金餡餅亦镶,那我是留著還是留著日月?這時你非常激動啊,你想著自己還沒娶媳婦缤骨,你就說那恭敬不如從命了爱咬,你拿著餡餅娶了一個漂亮能干的媳婦(消費掉了事件,onTouchEvent返回true)绊起,那這個事情就結束了精拟。當然,還有一種情況虱歪,你已經(jīng)有漂亮媳婦了蜂绎,不需要了,你覺得應該孝順長輩笋鄙,你又跟你爸說师枣,我不用啊,這金餅給我也沒用萧落,我有大金鏈子坛吁,你身體不好自己拿著看大夫吧。然后你爸拿著一想孩子說的沒錯铐尚,就自己拿去治病了(消費掉了)。
一個餡餅由一次觸摸事件的ACTION_DOWN開始哆姻,最先拿到的是Activity(Window)宣增,然后一層一層往下分發(fā)(dispatchTouchEvent),如果有誰需要拿到這個金餡餅干啥矛缨,他就攔截掉(onInterceptTouchEvent)爹脾,那么備份最小的你(View)就根本摸不到這個金餡餅了帖旨,如果沒有ViewGroup(比你輩分大的爺爺,爸爸等)攔截灵妨,都想給你娶媳婦解阅,,那么你就拿到了這個金餡餅泌霍,先調(diào)用你的onTouchEvent事件货抄,你確實不需要啊,然后又一層一層返回去朱转,一層層調(diào)用onTouchEvent蟹地,看誰需要,大致就這么一個邏輯藤为。
哇靠怪与,你這么說我似乎優(yōu)點明白了,但咱能不拿這個餡餅說事嗎缅疟,我開發(fā)又不是寫?zhàn)W餅分别,能不拿舉個別的例子啊。好的存淫,客官你別急耘斩,這就給你上菜。
滑動沖突想必是在開發(fā)中老生常談的問題了纫雁,只要我們內(nèi)部View和外部View都能滑動煌往,那么必定就會存在滑動沖突,我們想要處理的話轧邪,就需要用到我們的事件分發(fā)知識啦刽脖。而通常我們處理滑動沖突分為兩種,分別叫做外部攔截法和內(nèi)部攔截法忌愚,比如我們一個ViewPager中嵌套了一個RecycleView或ListView曲管,滑動時非常的不爽,安卓并不知道是具體誰要處理這個事件硕糊,金餅就一塊院水,我到底給誰啊,你們好幾個人都要简十。那我肯定需要添加一些條件了檬某,看看到底是給誰啊,外部攔截就是重寫父容器的onInterceptTouchEvent()方法螟蝙,因為這塊金餅先到的還是長輩手里恢恼,這個時候你就要 處理好啊,我到底是留給自己處理ViewPager的左右滑動呢胰默,還是處理ListView的上下滑動呀场斑,你只需要比較在X軸和Y軸移動的距離漓踢,如果X軸大于Y,那就是左右滑動漏隐,就把這塊金餅直接攔截掉消費了喧半,就不給ListView了,如果X小于Y青责,那就是上下滑動了挺据,你就不攔截,把金餅給ListView消費爽柒。
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int)event.getX();
int y = (int)event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (滿足父容器的攔截要求) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;}
以上是外部攔截法的模板代碼吴菠,針對不同的滑動沖突,只需要修改父容器需要攔截當前事件這個條件即可浩村,其他均不需做修改并且也不能修改做葵,在onInterceptTouchEvent方法中,首先是ACTION_DOWN這個事件心墅,父容器絕大部分情況下必須返回false酿矢,即不攔截ACTION_DOWN事件,這是因為一旦父容器攔截了ACTION_DOWN,那么后續(xù)的ACTION_MOVE和ACTION_UP事件都會直接交給父容器處理怎燥,這個時候事件沒法再傳遞給子元素了瘫筐;其次是ACTION_MOVE事件,這個事件可以根據(jù)需要來決定是否攔截铐姚,如果父容器需要攔截就返回true,否則返回false;最后是ACTION_UP事件策肝,這里必須要返回false,因為ACTION_UP事件本身沒有太多意義。
臥槽依许,那玩意我是一個ViewPager嵌套一個ViewPager呢棺禾,兩個都是水平方向的滑動,這個我要怎么判斷啊峭跳,這個.....這個貌似這種方式行不通吧膘婶,好像比較難啊,怎么去獲取判斷的條件啊蛀醉,依據(jù)是什么呀悬襟。
不要怕不要驚慌啊,我們肯定是可以解決的拯刁。我們外部攔截法行不通有沒有內(nèi)部攔截法古胆,自然是有的,內(nèi)部攔截法其實就是重寫子元素的dispatchTouchEvent()方法,并調(diào)用getParent().requestDisallowInterceptTouchEvent(true)父容器不能攔截子元素需要的事件逸绎。用我們的餡餅來說就是不管有多少長輩(ViewGroup父容器),餡餅都應該是先給你的(子元素)夭谤,你擁有燒餅的最先處理權棺牧,如果你需要消費它那你就直接消費掉,不需要再交給父容器處理朗儒。但是我們事件dispatchTouchEvent是由父輩們一層一層分發(fā)下來的颊乘,萬一哪個中間擺你一道,把餡餅拿去花掉了呢醉锄,為了預防這種情況乏悄,我們就需要配合getParent().requestDisallowInterceptTouchEvent(true)來事先通知他們不可以攔截。
首先是子元素的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event) {
...
switch (action) {
case MotionEvent.ACTION_MOVE:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if(父容器需要處理此事件)
getParent().requestDisallowInterceptTouchEvent(false);
break;
case MotionEvent.ACTION_UP: {
break;
}
...
return super.dispatchTouchEvent(event); }
這事件我們還要修改父容器的onInterceptTouchEvent()方法恳不,代碼如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action=ev.getAction();
if(action==MotionEvent.ACTION_DOWN){
return false;
}else {
return true;
}
}
父容器攔截了除了DOWN事件以外的其他事件檩小,這樣當子元素調(diào)用parent.requestDisallowInterceptTouchEvent(false)方法時,父元素才能繼續(xù)攔截所需的事件烟勋。當返回true時规求,不分發(fā)到子元素,并執(zhí)行自己的onTouch方法卵惦。onInterceptTouchEvent()方法默認是不攔截的阻肿,所以我們需要考慮到,當子元素不處理時沮尿,我們需要父元素(外層ViewPager來處理)丛塌,所以我們才會重寫父容器的onInterceptTouchEvent方法。
現(xiàn)在相信大家對于安卓Touch事件有了一個相對還比較清晰的了解了畜疾,至少知道他們的一個事件流向赴邻,分發(fā),攔截以及消費庸疾,這里在安卓開發(fā)探索一書中總結得特別好乍楚,大致如下:
1:同一個事件序列是指手機接觸屏幕那一刻起,到離開屏幕那一刻結束届慈,有一個down事件徒溪,若干個move事件,一個up事件構成金顿。
2:某個View一旦決定攔截事件臊泌,那么這個事件序列之后的事件都會由它來處理,并且不會再調(diào)用onInterceptTouchEvent揍拆。
3:正常情況下渠概,一個事件序列只能被一個View攔截并消耗。這個原因可以參考第2條,因為一旦攔截了某個事件播揪,那么這個事件序列里的其他事件都會交給這個View來處理贮喧,所以同一事件序列中的事件不能分別由兩個View同時處理,但是我們可以通過特殊手段做到猪狈,比如一個View將本該自己處理的事件通過onTouchEvent強行傳遞給其他View處理箱沦。
4:一個View如果開始處理事件,如果它不處理down事件(onTouchEvent里面返回了false),那么這個事件序列的其他事件就不會交給它來繼續(xù)處理了雇庙,而是會交給它的父元素去處理谓形。
5:如果一個View處理了down事件,卻沒有處理其他事件疆前,那么這些事件不會交給父元素處理寒跳,并且這個View還能繼續(xù)受到后續(xù)的事件。而這些未處理的事件竹椒,最終會交給Activity來處理童太。
6:ViewGroup的onInterceptToucheEvent默認返回false,也就是默認不攔截事件。
7:View沒有InterceptTouchEvent方法碾牌,如果有事件傳過來康愤,就會直接調(diào)用onTouchEvent方法。
8:View的onTouchEvent方法默認都會消耗事件舶吗,也就是默認返回true,除非他是不可點擊的(longClickable和clickable同時為false)征冷。
9:View的enable屬性不會影響onTouchEvent的默認返回值。就算一個View是不可見的誓琼,只要他是可點擊的(clickable或者longClickable有一個為true),它的onTouchEvent默認返回值也是true检激。
10:onClick方法會執(zhí)行的前提是當前View是可點擊的,并且它收到了down和up事件腹侣。
11:事件傳遞過程是由外向內(nèi)的叔收,也就是事件會先傳給父元素在向下傳遞給子元素。但是子元素可以通過requestDisallowInterceptTouchEvent來干預父元素的分發(fā)過程傲隶,但是down事件除外(因為down事件方法里饺律,會清除所有的標志位)。
我們用偽代碼表示一下分發(fā)跺株,攔截和消費的關系:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else {
consume = child.dispatchTouchEvent(ev);
}
return consume
}
如果以上知識复濒,你基本了解清楚了,那么針對各種情況下的滑動沖突乒省,你都能處理了巧颈,只是判斷的邏輯不同而已。這個就需要自己多加練習了袖扛,最近我也會不斷補強這一塊的知識砸泛,在實踐中不斷成長。
那么本文到此就結束了嗎,View的事件分發(fā)機制就是用來處理滑動沖突的嗎唇礁,隨便這已經(jīng)很了不起了但他能做的遠遠不止這些勾栗。
當我們的設計師用想象力沖破天機的思維告訴你想要什么什么樣的交互效果,我的內(nèi)心是崩潰的
那么通常這樣的交互效果需要我們時刻追蹤著用戶在屏幕上的一舉一動垒迂,然后獲取到用戶操作的坐標械姻,通過動畫等產(chǎn)生特定的效果,讓我們的用戶有一個爽歪歪的交互體驗机断。
這里我們就實現(xiàn)一個最簡單的下拉頭部圖片放大,松手時自動回彈的ScrollView吧绣夺。
臥槽我也不知道為什么錄制的gif這么小吏奸,反正大概就這樣子。
拿到這樣一個需求我們首先要分析他陶耍,解剖一下奋蔚。
(1)下拉時頭部變大
(2)松手后回彈,頭部回復大小
那么這兩個需求烈钞,可能用到哪些知識點來實現(xiàn)呢泊碑?
1:記錄下拉的值,下拉越大毯欣,頭部倍數(shù)越大馒过,在哪里一直記錄這個下拉值,自然就是我們今天學的咯酗钞,我們可以在dispatchTouchEvent或者onTouchEvent中獲取到我們觸摸點的坐標
2:View頭部的變大和回彈腹忽,需要動畫來達到一個流暢順滑有彈性的效果,而我們的補間代碼似乎無法滿足此類要求砚作,所以我們需要考慮用到一個ValueAnimator動畫窘奏,通過改變View對象的屬性來實現(xiàn)動畫效果。
3:放大應該有一個最大倍數(shù)葫录,不可能無限放大着裹,那太難看了
4:自定義ScrollView的話,我們需要獲取到ScrollView中的頭部這個View
5:縮放的話我們應該通過設置頭部LayoutParam改變
下面我就直接貼代碼了米同,大家可以自己參考一下:
public class PullBackScrollView extends ScrollView {
private View mHeaderView;
private int mHeaderWidth;
private int mHeaderHeight;
// 是否正在下拉
private boolean mIsPulling;
private int mLastY;
// 最大的放大倍數(shù)
private float mScaleTimes = 2.0f;
// 滑動放大系數(shù):系數(shù)越大骇扇,滑動時放大程度越大
private float mScaleRatio = 0.4f;
// 回彈時間系數(shù):系數(shù)越小,回彈越快
private float mReplyRatio = 0.5f;
// 當前坐標值
private float currentX = 0;
private float currentY = 0;
// 移動坐標值
private float distanceX = 0;
private float distanceY = 0;
// 最后坐標值
private float lastX = 0;
private float lastY = 0;
// 上下滑動標記
private boolean upDownSlide = false;
public static final String TAG = "PullBackScrollView";
public PullBackScrollView(Context context) {
this(context, null);
}
public PullBackScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PullBackScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// 設置不可過度滾動窍霞,否則上移后下拉會出現(xiàn)部分空白的情況
setOverScrollMode(OVER_SCROLL_NEVER);
View child = getChildAt(0);
if (child != null && child instanceof ViewGroup) {
// 獲取默認第一個子View
ViewGroup vg = (ViewGroup) getChildAt(0);
if (vg.getChildAt(0) != null) {
mHeaderView = vg.getChildAt(0);//此時headView為activity_header.xml中的RelativeLayout
}
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mHeaderWidth = mHeaderView.getMeasuredWidth();
mHeaderHeight = mHeaderView.getMeasuredHeight();
}
//重寫事件分發(fā)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
currentX = ev.getX();
currentY = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
distanceX = currentX - lastX;
distanceY = currentY - lastY;
if (Math.abs(distanceX) < Math.abs(distanceY) && Math.abs(distanceY) > 12) {
upDownSlide = true;
}
break;
}
lastX = currentX;
lastY = currentY;
if (upDownSlide && mHeaderView != null) {
commOnTouchEvent(ev);
}
return super.dispatchTouchEvent(ev);
}
/**
* @Description 觸摸事件
*/
private void commOnTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_UP:
// 手指離開后頭部恢復圖片
mIsPulling = false;
replyView();
clear();
break;
case MotionEvent.ACTION_MOVE:
if (!mIsPulling) {
// 第一次下拉
if (getScrollY() == 0) {
// 滾動到頂部時記錄位置匠题,否則正常返回
mLastY = (int) ev.getY();
} else {
break;
}
}
int distance = (int) ((ev.getY() - mLastY) * mScaleRatio);
// 當前位置比記錄位置要小時正常返回
if (distance < 0) {
break;
}
mIsPulling = true;
setZoom(distance);
break;
}
}
/**
* @Description 頭部縮放
*/
private void setZoom(float s) {
float scaleTimes = (float) ((mHeaderWidth + s) / (mHeaderWidth * 1.0));
// 如超過最大放大倍數(shù)則直接返回
if (scaleTimes > mScaleTimes) {
return;
}
ViewGroup.LayoutParams layoutParams = mHeaderView.getLayoutParams();
layoutParams.width = (int) (mHeaderWidth + s);
layoutParams.height = (int) (mHeaderHeight * ((mHeaderWidth + s) / mHeaderWidth));
// 設置控件水平居中
((MarginLayoutParams) layoutParams).setMargins(-(layoutParams.width - mHeaderWidth) / 2, 0, 0, 0);
mHeaderView.setLayoutParams(layoutParams);
}
/**
* @Description 回彈動畫
*/
private void replyView() {
final float distance = mHeaderView.getMeasuredWidth() - mHeaderWidth;
// 設置動畫
ValueAnimator anim = ObjectAnimator.ofFloat(distance, 0.0F).setDuration((long) (distance * mReplyRatio));
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
setZoom((Float) animation.getAnimatedValue());
}
});
anim.start();
}
/**
* @Description 清除屬性值
*/
private void clear() {
lastX = 0;
lastY = 0;
distanceX = 0;
distanceY = 0;
upDownSlide = false;
}}
在xml中引用我們的控件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.pz.zoomscrollview.MainActivity">
<com.example.pz.zoomscrollview.PullBackScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/activity_header" />
<include layout="@layout/activity_content" />
</LinearLayout>
</com.example.pz.zoomscrollview.PullBackScrollView></LinearLayout>
xml中的代碼我就不全部貼出來了,自己隨便寫點啥都可以但金,喜歡美女的放張美女背景圖韭山,喜歡跑車的放跑車。
按道理來說Activity代碼是什么都不需要寫的,你在xml中引用了自定義就好了钱磅,這里的話為了視覺效果梦裂,實現(xiàn)了一下沉浸式狀態(tài),就也貼出來參考一下實現(xiàn)盖淡。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
fullScreen(this);
}
private void fullScreen(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//5.x開始需要把顏色設置透明年柠,否則導航欄會呈現(xiàn)系統(tǒng)默認的淺灰色
Window window = activity.getWindow();
View decorView = window.getDecorView();
//兩個 flag 要結合使用,表示讓應用的主體內(nèi)容占用系統(tǒng)狀態(tài)欄的空間
int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(option);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
//導航欄顏色也可以正常設置
// window.setNavigationBarColor(Color.TRANSPARENT);
} else {
Window window = activity.getWindow();
WindowManager.LayoutParams attributes = window.getAttributes();
int flagTranslucentStatus = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
int flagTranslucentNavigation = WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
attributes.flags |= flagTranslucentStatus;
// attributes.flags |= flagTranslucentNavigation;
window.setAttributes(attributes);
}
}
}}
本文可能有一些知識點并未闡述得特別詳細褪迟,由于本人水平有限冗恨,也會在最近一段時間不短的學習相關知識以及更新一些相關文章,大家一起成長味赃。