Android觸控事件傳遞機制實踐——可拖動、大小切換的SizeSwitchView
前言
對於Android的觸控事件傳遞機制,網上有很多講解,有結合原始碼的,有圖文結合的,其中不乏一些講解清晰明瞭的文章,看完之後都能有所收穫。然而,理論終究是要應用在實踐上的,最近工作的時候,做出了一個可拖動,可以大小切換,大形態巢狀著ViewGroup的SizeSwitchView,其中涉及了比較複雜的觸控事件處理,實踐完之後我感覺對事件傳遞機制熟悉了很多,在這裡做出記錄,並分享給大家。
介紹
這個需求是做一個方向鍵,然後這個方向鍵有5個按鍵,整體比較大,可能會擋著其他的內容,然後就要求支援拖動和大小切換:
由於SizeSwitchView的可擴充套件性不高(大形態的ViewGroup可以是多種多樣的),要修改的話改動比較大,功能也不是很全面(只支援父ViewGroup為RelativeLayout),所以我把它定位為demo的方式分享出去,就不把它封裝並上傳到Jcenter了。
實現
1.結構分析
SizeSwitchView本質上是一個ViewGroup,裡面包含兩個互斥居中的控制元件(一個顯示,另外一個就不顯示),一個是小形態的控制元件,就只是一個ImageView,另一個是大形態的控制元件,是一個ViewGroup,裡面包含著5個ImageView,也就是下面的BigDirectionKey:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation ="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
>
<yanzhikai.sizeswitchview.BigDirectionKey
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:id ="@+id/big_dk"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:id="@+id/small_dk"
android:clickable="true"
android:scaleType="fitCenter"
android:src="@drawable/direction"
android:background="@drawable/background_button"
/>
</RelativeLayout>
2.狀態切換
關於大小形態的互相切換,主要注意的有兩個點:寬高和位置的變化、動畫的處理。
寬高和位置的變化
由於大形態和小形態的寬高不同,所以SizeSwitchView就要根據形態來變化大小,這裡使用修改LayoutParams的方式來修改大小和位置:
public void setMode(boolean isSmallMode){
this.isSmallMode = isSmallMode;
Log.d(TAG, "setMode: ");
//設定大小形態的寬高和位置
if (isSmallMode){
LayoutParams smallParams = (LayoutParams) getLayoutParams();
smallParams.width = mSmallWidth;
smallParams.height = mSmallHeight;
smallParams.leftMargin += (getWidth() - mSmallWidth)/2;
smallParams.bottomMargin += (getHeight() - mSmallHeight)/2;
setLayoutParams(smallParams);
}else {
LayoutParams bigParams = (LayoutParams) getLayoutParams();
bigParams.width = mBigWidth;
bigParams.height = mBigHeight;
bigParams.leftMargin -= (mBigWidth - mSmallWidth)/2;
bigParams.bottomMargin -= (mBigHeight - mSmallHeight)/2;
setLayoutParams(bigParams);
// requestLayout();
}
setKeysVisibility();
isDraggable = true;
}
通過getLayoutParams()
獲取LayoutParams來改動SizeSwitchView的寬高和位置,然後根據大小的寬高變化量來調整位置,使大小形態的中心點保持在同一個點上,這樣就讓人看起來是在中心點縮放變化一樣。
這個LayoutParams的型別取決於父ViewGroup,所以這裡就限定了父ViewGroup是要使用RelativeLayout(使用到Margin屬性)。
動畫的處理
這個切換的動畫實際上就是一個旋轉縮小透明度減少的動畫,加上反向旋轉放大透明度增加的動畫,這兩個組合起來(沒錯,就是在模仿宇智波帶土的神威)。。。
旋轉縮小透明度減少動畫shrink.xml:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="400">
<rotate
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%"
/>
<scale
android:fromXScale="1"
android:fromYScale="1"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="0"
android:toYScale="0" />
<alpha
android:fromAlpha="1"
android:toAlpha="0.3"
/>
</set>
反向旋轉放大透明度增加動畫:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="400">
<rotate
android:fromDegrees="0"
android:toDegrees="-360"
android:pivotX="50%"
android:pivotY="50%"
/>
<scale
android:fromXScale="0"
android:fromYScale="0"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="1"
android:toYScale="1" />
<alpha
android:fromAlpha="0.3"
android:toAlpha="1"
/>
</set>
下面是動畫的設定:
//初始化動畫
private void initAnim(){
smallShrinkAnimation = AnimationUtils.loadAnimation(mContext,R.anim.shrink);
bigLargenAnimation = AnimationUtils.loadAnimation(mContext,R.anim.largen);
smallShrinkAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
setKeysClickable(false);
isDraggable = false;
}
@Override
public void onAnimationEnd(Animation animation) {
setMode(false);
startAnimation(bigLargenAnimation);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
bigLargenAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
setKeysClickable(true);
isDraggable = true;
setSmallKeyClick();
checkBoundary();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
bigShrinkAnimation = AnimationUtils.loadAnimation(mContext,R.anim.shrink);
smallLargenAnimation = AnimationUtils.loadAnimation(mContext,R.anim.largen);
bigShrinkAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
setKeysClickable(false);
isDraggable = false;
}
@Override
public void onAnimationEnd(Animation animation) {
setMode(true);
startAnimation(smallLargenAnimation);
setSmallKeyClick();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
smallLargenAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
setKeysClickable(true);
isDraggable = true;
checkBoundary();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
}
上面主要是4個動畫(變大變小各兩個)監聽器的實現,思路就是:動畫開始的時候不能點選,不能拖動;等到縮小動畫完畢之後瞬間切換大小狀態,再進行放大動畫;動畫都結束後就是恢復可以點選和可拖動狀態,還有進行一次邊界檢測,看變換後的View是否越出了父View的邊界,越出了的話就移動越出的位移,這個checkBoundary()
方法的實現在後面講。
3.觸控事件處理
由於SizeSwitchView需要支援拖動,需要實現攔截觸控事件,但是它也是一個父ViewGroup,還需要把點選事件(DOWN和UP事件)傳遞到子ViewGroup裡面的View。瞭解過Android觸控事件傳遞機制的都知道,如果父View攔截了DOWN事件之後,後面的事件就不會傳遞到它的子View了。
所以我在SizeSwitchView的攔截思路是這樣的:
其實也不復雜,就是讓父ViewGroup只有在拖動達到一定距離的時候才攔截MOVE事件,DOWN事件就傳遞給子View處理,但是這樣有一個問題:在父ViewGroupOnTouchEvent()
方法是沒有DOWN事件的,不能在這裡獲取DOWN事件的座標,MotionEvent.getX()
和MotionEvent.getY()
方法獲取的只是點選事件相對於當前View的零點的位置,而不是在螢幕上的XY座標,所以光靠MOVE事件的位置資料是無法準確計算SizeSwitchView的移動的(每次都要平移到View的零點才能正常拖動),如下面的效果:
所以就直接在onInterceptTouchEvent()
裡面獲取DOWN事件的座標,用全域性變數儲存著,用MOVE事件的座標減去它,才能正確計算出它的位移,從而進行準確移動:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
//記錄DOWN事件的點選位置,因為不攔截DOWN事件,移動的時候需要這個起點座標來計算距離。
lastX = ev.getX();
lastY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
//拖動距離超過最小拖動量才會被拖動
if (Math.abs(ev.getX() - lastX) > clickOffset && Math.abs(ev.getY() - lastY )> clickOffset){
if (canDrag && isDraggable && ev.getAction() == MotionEvent.ACTION_MOVE){
return true;
}
}
break;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP){
//擡手就進行一次邊界檢測
checkBoundary();
}else if (event.getAction() == MotionEvent.ACTION_MOVE){
//進行移動操作
if (canDrag && isDraggable) {
int offX = (int) (event.getX() - lastX);
int offY = (int) (event.getY() - lastY);
LayoutParams params =
(LayoutParams) getLayoutParams();
params.leftMargin = params.leftMargin + offX;
params.rightMargin = params.rightMargin - offX;
params.topMargin += offY;
params.bottomMargin -= offY;
setLayoutParams(params);
return true;
}
return false;
}
return super.onTouchEvent(event);
}
然後就是邊界檢測了,具體思路很簡單,就是計算SizeSwitchView當前位置是不是超出它的父ViewGroup的範圍,如果超過的話就要移回來:
//檢測View是否跑出邊界,如果是則移回來
private void checkBoundary(){
Log.d("checkBoundary", "checkBoundary: ");
ViewGroup parent = (ViewGroup) getParent();
boolean isOut = false;
int moveX = 0;
int moveY = 0;
if (getLeft() < 0){
moveX = getLeft();
isOut = true;
}
if (getTop() < 0){
moveY = getTop();
isOut = true;
}
if (getRight() > parent.getWidth()){
moveX = (getRight() - parent.getWidth());
isOut = true;
}
if (getBottom() > parent.getHeight()){
moveY = (getBottom() - parent.getHeight());
isOut = true;
}
//有出界才進行LayoutParams的設定,節省效能
if (isOut) {
LayoutParams params =
(LayoutParams) getLayoutParams();
params.setMargins(params.leftMargin - moveX,
params.topMargin - moveY,
params.rightMargin + moveX,
params.bottomMargin + moveY);
setLayoutParams(params);
}
}
這樣的話拖動的功能就處理好了,點選事件也傳進去子ViewGroup了,但是怎樣把子ViewGroup的點選實現實現介面傳出來呢?這就和我上一篇的YMenuView的設計差不多了:由於在大形態的BigDirectionKey有5個子View,所以就自己實現一個帶index索引的介面OnKeyClickListener去實現點選,外層呼叫的話只需要實現這個OnKeyClickListener然後傳入進來就行了。
private OnKeyClickListener mOnKeyClickListener;
//初始化
private void initKeys(){
okKey = new ImageView(mContext);
upKey = new ImageView(mContext);
downKey = new ImageView(mContext);
leftKey = new ImageView(mContext);
rightKey = new ImageView(mContext);
okKey.setImageResource(R.drawable.background_ok);
upKey.setImageResource(R.drawable.background_up);
downKey.setImageResource(R.drawable.background_down);
leftKey.setImageResource(R.drawable.background_left);
rightKey.setImageResource(R.drawable.background_right);
okKey.setClickable(true);
upKey.setClickable(true);
downKey.setClickable(true);
leftKey.setClickable(true);
rightKey.setClickable(true);
okKey.setScaleType(ImageView.ScaleType.FIT_XY);
upKey.setScaleType(ImageView.ScaleType.FIT_XY);
downKey.setScaleType(ImageView.ScaleType.FIT_XY);
leftKey.setScaleType(ImageView.ScaleType.FIT_XY);
rightKey.setScaleType(ImageView.ScaleType.FIT_XY);
okKey.setId(generateViewId());
upKey.setId(generateViewId());
downKey.setId(generateViewId());
leftKey.setId(generateViewId());
rightKey.setId(generateViewId());
addView(okKey);
addView(upKey);
addView(downKey);
addView(leftKey);
addView(rightKey);
setBackgroundResource(R.drawable.button_shape);
//設定點選監聽器
for (int i = 0; i < getChildCount(); i++){
getChildAt(i).setOnClickListener(new MyOnClickListener(i));
}
}
//重寫一個帶索引的OnClickListener,索引用於標識5個子View
private class MyOnClickListener implements OnClickListener {
private int index;
public MyOnClickListener(int index) {
this.index = index;
}
@Override
public void onClick(View v) {
if (mOnKeyClickListener != null) {
mOnKeyClickListener.onKeyClick(index);
}
}
}
//暴露給外部的點選介面
public interface OnKeyClickListener{
public void onKeyClick(int index);
}
這樣子,在Activity裡面只需要實現BigDirectionKey.OnKeyClickListener介面然後重寫裡面的方法就可以處理點選事件了:
@Override
public void onKeyClick(int index) {
switch (index){
case 0:
mSizeSwitchView.toSmallMode();
break;
case 1:
makeToast("1");
break;
case 2:
makeToast("2");
break;
case 3:
makeToast("3");
break;
case 4:
makeToast("4");
break;
}
}
總結
這樣就介紹完了SizeSwitchView大體實現思路了,總體來說就是點選、切換、拖動。其實難點並不多,但是實現的時間還是不短的,就是實現的時候遇到搞不定的功能會各種各種的嘗試,最後才得到解決方法,而且思路還是很亂,經過總結原理之後發現很多可以改善的地方,小改的地方我都優化了,可以大改的地方,如換一種移動的方式(使用layout()
方法的方式,可以大大減少Measure的次數,讓拖動更平滑),這個改動比較大,留到後面。
後話
這個SizeSwitchView和上一篇的YMenuView都是屬於我做的一個專案,目前專案處於總結階段,在實現功能中學到很多東西,做完過段時間將這些東西整理一遍,又改進了一遍,自己理解得又更深了。
相關推薦
Android觸控事件傳遞機制實踐——可拖動、大小切換的SizeSwitchView
前言 對於Android的觸控事件傳遞機制,網上有很多講解,有結合原始碼的,有圖文結合的,其中不乏一些講解清晰明瞭的文章,看完之後都能有所收穫。然而,理論終究是要應用在實踐上的,最近工作的時候,做出了一個可拖動,可以大小切換,大形態巢狀著ViewG
Android 觸控事件傳遞機制
android系統中的每個View的子類都具有下面三個和TouchEvent處理密切相關的方法:1)public boolean dispatchTouchEvent(MotionEvent ev) 這個方法用來分發TouchEvent2)public boolean onInterceptTouchEve
android觸控事件傳遞機制
看到一篇文章,將事件傳遞機制講得很透徹 【場景】 在cy的Home頁,每個點選塊都是自定義view來做的,組要用自定義自合view,如果需要將其中點選image和text分別有不同的反應,則應該去設定處理事件? 【詳情】 Android系統中的每個View的子類都具有下面三
初識Android觸控事件傳遞機制
前言 今天總結的一個知識點是Andorid中View事件傳遞機制,也是核心知識點,相信很多開發者在面對這個問題時候會覺得困惑,另外,View的另外一個難題滑動衝突,比如在ScrollView中巢狀ListView,都是上下滑動,這該如何解決呢,它解決的依據就是View事件的傳遞機制,所以開發者需要對View的
Android觸控事件傳遞機制簡要分析
Android開發中經常會遇到多個View、ViewGroup巢狀的情況, 此時就可能遇到滑動衝突的問題。 為了這種問題,就必須對View的事件傳遞機制有一定的瞭解。 本篇部落格就以一些簡單的例子, 來看看Activity、View、ViewGroup三
android 觸控事件傳遞機制與筆記
一、筆記連結1. android 觸控事件傳遞機制2. android OnTouchListener,onTouchEvent,onClickListener執行順序 二、簡記1. android 觸控事件傳遞機制1.1Touch事件分發中只有兩個主角:ViewGroup和
Android觸控事件傳遞機制學習筆記
1、Android 觸控事件傳遞機制 http://blog.csdn.net/awangyunke/article/details/22047987 2、Android-onInterceptTouchEvent()和onTouchEvent()總結 h
Android觸控事件傳遞機制,這一篇就夠了
整個觸控事件牽涉到的是,Activity,View,ViewGroup三者的傳遞機制。 這個觸控事件就是從外層往內層一層層的傳遞。 整個傳遞機制,分為3個步驟:分發,攔截,和消費。 1. 觸控事件的型別 事件型別是MotionEvent類:看下最新的sdk29的原始碼,一堆的Action,我們常用的其實就3個
Android ViewGroup 觸控事件傳遞機制
引言 上一篇部落格我們學習了Android View 觸控事件傳遞機制,不瞭解的同學可以檢視Android View 觸控事件傳遞機制。今天繼續學習Android觸控事件傳遞機制,這篇部落格將和大家一起探討ViewGroup的觸控事件傳遞機制。 示例
android view觸控事件傳遞機制測試
沒有其它人為干預時: 詳細測試可以參考Github中的程式,地址: https://github.com/yifan42421/PhoneToPhoneScreen/tree/master/testmotionevent
Android Touch事件傳遞機制全面解析(從WMS到View樹)
了解 分支 per seve from 這一 params 雞湯 dcl 轉眼間近一年沒更新博客了,工作一忙起來。非常難有時間來寫博客了,因為如今也在從事Andro
Android View事件傳遞機制
view事件傳遞機制,在很多面試中會問道,我曾經也被問道,卻沒有回答上來。 今天我在這裡寫了一個demo去理解這個view的事件傳遞機制。 首先這個view包括兩種,viewGroup和普通view。viewGroup就是裡面還可以包含子控制元件的那種,如Linear
android 觸控事件傳遞(一)
android 觸控事件傳遞 1、主要相關程式碼路徑 基於展訊7.0原始碼 native frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp frameworks
Android TouchEvent事件傳遞機制
public class MyActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState)
安卓觸控事件傳遞機制
概述 安卓的觸控事件傳遞大體上是檢視收到事件後進行決定是否要攔截,不攔截可以繼續向內傳遞,攔截了不消費也可以回傳給上層檢視。 事件型別主要有 ACTION_DOWN(按下) ACTION_MOVE(移動) ACTION_UP(擡起) ACTION_C
Android 觸控事件傳遞流程解析
android中的Touch事件都是從ACTION_DOWN開始的: 單手指操作:ACTION_DOWN---ACTION_MOVE----ACTION_UP 多手指操作:ACTION_DOWN---ACTION_POINTER_DOWN---ACTION_MOV
Android onTouch事件傳遞機制
Android onTouch事件介紹: Android的觸控事件:onClick, onScroll, onFling等等,都是由許多個Touch組成的。其中Touch的第一個狀態肯定是ACTION_DOWN, 表示按下了螢幕。之後,touch將會有後續事件,可能是: ACT
觸控[0] 觸控事件傳遞機制
【參考連結】 開發藝術探索 1. 涉及到的類和方法主要有 ViewGroup的dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent() View的dispatchTouchEvent()、onInterceptTouchEvent()
android觸發事件傳遞機制
一 事件傳遞的三個階段1 分發(Dispatch):事件的分發對應著dispatchTouchEvent方法,在Android系統中,所有的觸控事件的分發都是由改方法分發。 public boole
Android Touch事件傳遞機制解析
// 表示事件是否攔截, 返回false表示不攔截 @Override public boolean onInterceptTouchEvent(MotionEvent arg0) { return false; } /** * 重寫onTouchEven