Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 關於android開發 >> React-Native系列Android——Touch事件原理及狀態效果

React-Native系列Android——Touch事件原理及狀態效果

編輯:關於android開發

React-Native系列Android——Touch事件原理及狀態效果


Native原生相比於HybridH5最大優點是具有流暢和復雜的交互效果,觸摸事件便是其中重要一項,包括點擊(Click)、長按(LongClick)、手勢(gesture)等。

以最簡單常見的點擊(Click)為例,Native組件可以自定義selector,使得被點擊的組件具有動態效果,Android 5.0以上甚至可以有漣漪效果(Material Design)。而這些在HybridH5中很難實現,很多時候區分它們與原生最簡單的方法就是檢驗點擊交互效果。

React-Native的強大之處在於實現了較為全面的Touch事件機制,雖然仍略有缺陷,但相比於HybridH5的體驗而言,已經足足提高了一大截,下面分析講解一下其實現原理,和具體使用方式。


1、Touch事件機制

如果閱讀過React-Native源碼的話,應該了解React-Native頁面的UI根視圖是ReactRootView,包路徑是:com.facebook.react.ReactRootView,它是FramLayout的一個子類。

首先,來看一下ReactActivity這個頁面基類,ReactRootView是如何作為React-Native的根視圖被初始化及添加的。

public abstract class ReactActivity extends Activity implements DefaultHardwareBackBtnHandler {

...

  protected ReactRootView createRootView() {
    return new ReactRootView(this);
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    if (getUseDeveloperSupport() && Build.VERSION.SDK_INT >= 23) {
      // Get permission to show redbox in dev builds.
      if (!Settings.canDrawOverlays(this)) {
        Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
        startActivity(serviceIntent);
        FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
        Toast.makeText(this, REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
      }
    }

    mReactInstanceManager = createReactInstanceManager();
    ReactRootView mReactRootView = createRootView();
    mReactRootView.startReactApplication(mReactInstanceManager, getMainComponentName(), getLaunchOptions());
    setContentView(mReactRootView);
  }

...

}

ReactActivityonCreate這個生命周期裡,直接實列化,然後作為當前WindowContentView,也就可以認為其是所有React-Native組件的根視圖。

熟悉Android觸摸事件機制的,應該知道視圖樹中,觸摸事件是逐級傳遞的,每個視圖(View)中有兩個接收和處理Touch事件的方法,分別是onInterceptTouchEventonTouchEvent,這兩個方法的區別為:

onInterceptTouchEvent的傳遞層級是由父視圖向子視圖,顧名思義,通常用作事件攔截。
onTouchEvent的傳遞層級是由子視圖向父視圖,通常用作事件處理。

我們來看一下ReactRootView的事件接收和處理。

public class ReactRootView extends SizeMonitoringFrameLayout implements RootView {
   ...

     @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    handleTouchEvent(ev);
    return super.onInterceptTouchEvent(ev);
  }

  @Override
  public boolean onTouchEvent(MotionEvent ev) {
    handleTouchEvent(ev);
    super.onTouchEvent(ev);
    // In case when there is no children interested in handling touch event, we return true from
    // the root view in order to receive subsequent events related to that gesture
    return true;
  }

   ...
}

很明顯,這裡onInterceptTouchEventonTouchEvent的處理都是全部交給handleTouchEvent方法統一處理的。

我們再繼續看一下handleTouchEvent方法。

public class ReactRootView extends SizeMonitoringFrameLayout implements RootView {
   ...

    /**
   * Main catalyst view is responsible for collecting and sending touch events to JS. This method
   * reacts for an incoming android native touch events ({@link MotionEvent}) and calls into
   * {@link com.facebook.react.uimanager.events.EventDispatcher} when appropriate.
   * It uses {@link com.facebook.react.uimanager.TouchTargetManagerHelper#findTouchTargetView}
   * helper method for figuring out a react view ID in the case of ACTION_DOWN
   * event (when the gesture starts).
   */
  private void handleTouchEvent(MotionEvent ev) {

   ...

   int action = ev.getAction() & MotionEvent.ACTION_MASK;
    ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
    EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class)
        .getEventDispatcher();
    if (action == MotionEvent.ACTION_DOWN) {

    ...

    mTargetTag = TouchTargetHelper.findTargetTagAndCoordinatesForTouch(
          ev.getX(),
          ev.getY(),
          this,
          mTargetCoordinates);    

    eventDispatcher.dispatchEvent(
          TouchEvent.obtain(
              mTargetTag,
              SystemClock.uptimeMillis(),
              TouchEventType.START,
              ev,
              mTargetCoordinates[0],
              mTargetCoordinates[1]));
    } else if (action == MotionEvent.ACTION_UP) {
      // End of the gesture. We reset target tag to -1 and expect no further event associated with
      // this gesture.
      eventDispatcher.dispatchEvent(
          TouchEvent.obtain(
              mTargetTag,
              SystemClock.uptimeMillis(),
              TouchEventType.END,
              ev,
              mTargetCoordinates[0],
              mTargetCoordinates[1]));
      mTargetTag = -1;
    } else if (action == MotionEvent.ACTION_MOVE) {
      // Update pointer position for current gesture
      eventDispatcher.dispatchEvent(
          TouchEvent.obtain(
              mTargetTag,
              SystemClock.uptimeMillis(),
              TouchEventType.MOVE,
              ev,
              mTargetCoordinates[0],
              mTargetCoordinates[1]));
    } else if (action == MotionEvent.ACTION_POINTER_DOWN) {
      // New pointer goes down, this can only happen after ACTION_DOWN is sent for the first pointer
      eventDispatcher.dispatchEvent(
          TouchEvent.obtain(
              mTargetTag,
              SystemClock.uptimeMillis(),
              TouchEventType.START,
              ev,
              mTargetCoordinates[0],
              mTargetCoordinates[1]));
    } else if (action == MotionEvent.ACTION_POINTER_UP) {
      // Exactly onw of the pointers goes up
      eventDispatcher.dispatchEvent(
          TouchEvent.obtain(
              mTargetTag,
              SystemClock.uptimeMillis(),
              TouchEventType.END,
              ev,
              mTargetCoordinates[0],
              mTargetCoordinates[1]));
    } else if (action == MotionEvent.ACTION_CANCEL) {
      dispatchCancelEvent(ev);
      mTargetTag = -1;
    }
  }
   ...
}

代碼不是很多,也很好理解。先來看一下注釋,意思是ReactRootView 負責收集和發送事件給JS,當原生觸摸事件響應時通過EventDispatcher類發送,並且在Down事件時通過TouchTargetManagerHelper查找具體被觸摸的子View。這裡一語道破了觸摸事件的核心原理:

所有React組件的觸摸事件都是由ReactRootView統一處理,將具體被觸摸組件和具體觸摸事件發送給Javascript。其中隱藏的一層意思是:React組件自身不用處理觸摸事件。

這個很關鍵,而具體被處理的觸摸事件有以下6種,分別是ACTION_DOWN、ACTION_UP、ACTION_MOVE、ACTION_POINTER_DOWN、ACTION_POINTER_UP、ACTION_CANCEL,已經包含了幾乎所有的手勢動作。


2、Touch事件接收者

接下來,看一下ACTION_DOWN事件時,是如何定位消費Touch事件React組件的呢?以下圖為例:

\

如果黃色的點表示被觸摸的事件點,由於Touch事件是由ReactRootView根節點開始攔截,所以從ReactRootView開始遍歷視圖樹,遍歷順序如下:

1、ReactViewGroup,判斷黃點坐標位於ReactViewGroup區域,再判斷ReactViewGroup自身或其子視圖是否消費Touch事件(通過PointerEvents枚舉類,後面詳解)。如果自身消費Touch事件,遍歷中斷,直接返回ReactGroupView;如果子視圖消費Touch事件,繼續遍歷其子視圖樹;如果不消費Touch事件,返回null

2、child 1,如果ReactViewGroup的子視圖消費Touch事件,則遍歷至child 1,首先判斷黃點坐標是否位於child 1區域,再判斷自身或其子視圖是否消費Touch事件。如果child 1ReactViewGroup類型,同上方1過程處理;如果child 1是非ReactViewGroup類型,即ImageViewTextView等非復合型視圖,判斷其自身是否消費Touch事件,一般除具有Span屬性的TextView外,基本都是消費Touch事件的。如果消費Touch事件,返回child 1,如果不消費Touch事件,返回null

3、child 2,如果2child 1不消費Touch事件,繼續遍歷到child 2,由於觸摸點黃點坐標不位於child 2區域內,遍歷終止,返回null

關於視圖是否消費Touch事件,通過一個枚舉類來說明,代碼位於com.facebook.react.uimanager.PointerEvents
一共有4種枚舉類型:

NONE:視圖自身或其子視圖不消費Touch事件。
BOX_NONE:視圖自身不消費Touch事件,但其子視圖消費。
BOX_ONLY:視圖自身消費Touch事件,而其子視圖不消費。
AUTO:視圖自身或其子視圖消費Touch事件,但不確定是哪一個。

關於具體如何查找事件消費者的代碼主要在com.facebook.react.uimanager.TouchTargetHelper中。

/**
 * Class responsible for identifying which react view should handle a given {@link MotionEvent}.
 * It uses the event coordinates to traverse the view hierarchy and return a suitable view.
 */
public class TouchTargetHelper {

   ...

  /**
   * Find touch event target view within the provided container given the coordinates provided
   * via {@link MotionEvent}.
   *
   * @param eventX the X screen coordinate of the touch location
   * @param eventY the Y screen coordinate of the touch location
   * @param viewGroup the container view to traverse
   * @param viewCoords an out parameter that will return the X,Y value in the target view
   * @return the react tag ID of the child view that should handle the event
   */
  public static int findTargetTagAndCoordinatesForTouch(
      float eventX,
      float eventY,
      ViewGroup viewGroup,
      float[] viewCoords) {
    UiThreadUtil.assertOnUiThread();
    int targetTag = viewGroup.getId();
    // Store eventCoords in array so that they are modified to be relative to the targetView found.
    viewCoords[0] = eventX;
    viewCoords[1] = eventY;
    View nativeTargetView = findTouchTargetView(viewCoords, viewGroup);
    if (nativeTargetView != null) {
      View reactTargetView = findClosestReactAncestor(nativeTargetView);
      if (reactTargetView != null) {
        targetTag = getTouchTargetForView(reactTargetView, viewCoords[0], viewCoords[1]);
      }
    }
    return targetTag;
  }

   ...

}

具體有三層邏輯:findTouchTargetViewfindClosestReactAncestorgetTouchTargetForView,最終是要返回目標ViewID,代碼我們一一來看。

1、findTouchTargetView

/**
   * Returns the touch target View that is either viewGroup or one if its descendants.
   * This is a recursive DFS since view the entire tree must be parsed until the target is found.
   * If the search does not backtrack, it is possible to follow a branch that cannot be a target
   * (because of pointerEvents). For example, if both C and E can be the target of an event:
   * A (pointerEvents: auto) - B (pointerEvents: box-none) - C (pointerEvents: none)
   *  \ D (pointerEvents: auto)  - E (pointerEvents: auto)
   * If the search goes down the first branch, it would return A as the target, which is incorrect.
   * NB: This modifies the eventCoords to always be relative to the current viewGroup. When the
   * method returns, it will contain the eventCoords relative to the targetView found.
   */
  private static View findTouchTargetView(float[] eventCoords, ViewGroup viewGroup) {
    int childrenCount = viewGroup.getChildCount();
    for (int i = childrenCount - 1; i >= 0; i--) {
      View child = viewGroup.getChildAt(i);
      PointF childPoint = mTempPoint;
      if (isTransformedTouchPointInView(eventCoords[0], eventCoords[1], viewGroup, child, childPoint)) {
        // If it is contained within the child View, the childPoint value will contain the view
        // coordinates relative to the child
        // We need to store the existing X,Y for the viewGroup away as it is possible this child
        // will not actually be the target and so we restore them if not
        float restoreX = eventCoords[0];
        float restoreY = eventCoords[1];
        eventCoords[0] = childPoint.x;
        eventCoords[1] = childPoint.y;
        View targetView = findTouchTargetViewWithPointerEvents(eventCoords, child);
        if (targetView != null) {
          return targetView;
        }
        eventCoords[0] = restoreX;
        eventCoords[1] = restoreY;
      }
    }
    return viewGroup;
}

循環遍歷ReactRootView 的視圖樹,通過isTransformedTouchPointInView方法判斷觸摸點坐標是否位於當前遍歷子視圖的區域內。有一點需要特別注意,就是坐標的處理。默認的觸摸點坐標是基於ReactRootView 的坐標系參照,如果遍歷到子視圖,需要將觸摸點坐標轉換成以子視圖為坐標系參照的坐標。主要是通過上方代碼中的childPoint變量保存和處理的。
觸摸點坐標是否位於當前遍歷子視圖的區域內,通過findTouchTargetViewWithPointerEvents方法,判斷當前遍歷子視圖是否消費Touch事件。

/**
   * Returns the touch target View of the event given, or null if neither the given View nor any of
   * its descendants are the touch target.
   */
  private static @Nullable View findTouchTargetViewWithPointerEvents(
      float eventCoords[], View view) {
    PointerEvents pointerEvents = view instanceof ReactPointerEventsView ?
        ((ReactPointerEventsView) view).getPointerEvents() : PointerEvents.AUTO;
    if (pointerEvents == PointerEvents.NONE) {
        return null;
    } else if (pointerEvents == PointerEvents.BOX_ONLY) {
        return view;
    } else if (pointerEvents == PointerEvents.BOX_NONE) {
      if (view instanceof ViewGroup) {
        View targetView = findTouchTargetView(eventCoords, (ViewGroup) view);
        if (targetView != view) {
          return targetView;
        }
        ...
      }
      return null;

    } else if (pointerEvents == PointerEvents.AUTO) {
      // Either this view or one of its children is the target
      if (view instanceof ViewGroup) {
        return findTouchTargetView(eventCoords, (ViewGroup) view);
      }
      return view;

    } else {
      throw new JSApplicationIllegalArgumentException(
          "Unknown pointer event type: " + pointerEvents.toString());
    }
  }

findTouchTargetViewWithPointerEvents方法對PointerEvents的四種枚舉做了相應處理,NONE返回nullBOX_ONLY返回當前視圖,BOX_NONEAUTO繼續遍歷,遞歸調用了findTouchTargetView

2、findClosestReactAncestor

  private static View findClosestReactAncestor(View view) {
    while (view != null && view.getId() <= 0) {
      view = (View) view.getParent();
    }
    return view;
  }

由於查找最終是要返回目標視圖的ID,如果目標視圖的ID非法小於0,則返回其父視圖作為替代。此處作用不是很理解,忘解答,感激不盡。

3、getTouchTargetForView

  private static int getTouchTargetForView(View targetView, float eventX, float eventY) {
    if (targetView instanceof ReactCompoundView) {
      // Use coordinates relative to the view, which have been already computed by
      // {@link #findTouchTargetView()}.
      return ((ReactCompoundView) targetView).reactTagForTouch(eventX, eventY);
    }
    return targetView.getId();
  }

這個方法是針對ReactTextView做特殊處理的,由於ReactTextView中可能存在消費Touch事件的Span,如果有則返回其Spantag值(具體請閱讀ReactTextViewReactTagSpan)。


3、Touch事件發送

代碼位於com.facebook.react.uimanager.events.EventDispatcher中,先來看一下EventDispatcher對象的初始化。

public class EventDispatcher implements LifecycleEventListener {

     ...

     private final ReactApplicationContext mReactContext;
     private @Nullable RCTEventEmitter mRCTEventEmitter;
     private volatile @Nullable ScheduleDispatchFrameCallback mCurrentFrameCallback;

     public EventDispatcher(ReactApplicationContext  reactContext) {
      mReactContext = reactContext;
      mReactContext.addLifecycleEventListener(this);
    }

  @Override
  public void onHostResume() {
    UiThreadUtil.assertOnUiThread();
    Assertions.assumeCondition(mCurrentFrameCallback == null);

    if (mRCTEventEmitter == null) {
      mRCTEventEmitter = mReactContext.getJSModule(RCTEventEmitter.class);
    }

    mCurrentFrameCallback = new ScheduleDispatchFrameCallback();
    ReactChoreographer.getInstance().postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, mCurrentFrameCallback);
  }

     ...

}

EventDispatcher實現了LifecycleEventListener接口,在ReactActivity的各個生命周期執行時回調給EventDispatcher

onHostResume方法對應ActivityonResume生命周期,主要通過ReactChoreographer單例來post了一個ScheduleDispatchFrameCallback。而ReactChoreographer是對Choreographer的一層封裝,這裡可以直接看成是ChoreographerChoreographer是一個消息處理器 。

ScheduleDispatchFrameCallbackEventDispatcher的一個內部Choreographer.FrameCallback實現類。接下來看看ScheduleDispatchFrameCallback這個回調類裡面處理了哪些東西。

private class ScheduleDispatchFrameCallback implements Choreographer.FrameCallback {

    private boolean mShouldStop = false;

    @Override
    public void doFrame(long frameTimeNanos) {
      UiThreadUtil.assertOnUiThread();

      if (mShouldStop) {
        return;
      }

     ...

      try {
        moveStagedEventsToDispatchQueue();

        if (!mHasDispatchScheduled) {
          mHasDispatchScheduled = true;
          ...
          mReactContext.runOnJSQueueThread(mDispatchEventsRunnable);
        }

        ReactChoreographer.getInstance()
            .postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, this);
      } finally {
        Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
      }
    }

    public void stop() {
      mShouldStop = true;
    }
  }

doFrame的回調方法裡面一共做了兩件事情:
1、將mDispatchEventsRunnable塞進Javascript處理線程的隊列中,其內部是通過Handler消息機制關聯到主線程做處理的。
2、遞歸調用,再次post了當前ScheduleDispatchFrameCallback,達到一個循環的目的。

接下裡,我們看看mDispatchEventsRunnable是如何發送Touch事件的。

 private class DispatchEventsRunnable implements Runnable {

    @Override
    public void run() {
      ...
      try {
        ...
        mHasDispatchScheduled = false;
        mHasDispatchScheduledCount++;
        ...
        synchronized (mEventsToDispatchLock) {
          ...
          for (int eventIdx = 0; eventIdx < mEventsToDispatchSize; eventIdx++) {
            Event event = mEventsToDispatch[eventIdx];
            // Event can be null if it has been coalesced into another event.
            if (event == null) {
              continue;
            }
            ...
            event.dispatch(mRCTEventEmitter);
            event.dispose();
          }
          clearEventsToDispatch();
          mEventCookieToLastEventIdx.clear();
        }
      } finally {
        Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
      }
    }
  }

DispatchEventsRunnable這個對象的作用,只有一個:循環遍歷mEventsToDispatch數組,然後調用eventdispatch方法發送給Javascript。這裡涉及到RCTEventEmitter的一個JS組件類,裡面有一個receiveEvent(int targetTag, String eventName, WritableMap event)的方法用來與JS交互的,這裡不做深入分析,下一篇博客會以此為例詳解,敬請關注!

梳理一下,也就是說所有的Touch事件都會預先存入mEventsToDispatch數組裡,然後在每次ScheduleDispatchFrameCallback回調後,使用DispatchEventsRunnable最終將Touch事件傳遞給JS。

Touch事件如何預先存入mEventsToDispatch數組中,則是通過onInterceptTouchEvent->handleTouchEvent->dispatchEvent->moveStagedEventsToDispatchQueue->addEventToEventsToDispatch的流程運行的,裡面還會有一個mEventStaging暫存的過程,比較簡單,不再講解。


4、Touch事件在React-Native中的使用

既然Javascript能夠接收到原生native端的幾乎所有Touch事件,那麼就可以做出很多復雜的交互效果了,以點擊(click)事件為例,演示下React-Native的幾種交互效果。

4-1 普通觸摸效果

點擊文本,出現‘Awesome, Clicking!’的點擊Toast提示,這是最簡單和常用的點擊功能,直接使用onPress屬性實現。

function onClick(){
   var ToastAndroid = require('ToastAndroid')
   ToastAndroid.show('Awesome, Clicking!', ToastAndroid.SHORT);
}

class MyProject extends Component {
  render() {
    return (
      
        Click Me!
      
    );
  }
}

\

4-2 變色觸摸效果

使用TouchableHighlight組件實現,點擊瞬間或者長按時,可以設定一個顏色視覺差。TouchableHighlight<喎?http://www.Bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vc3Ryb25nPrHqx6mx2NDrsPy5/LG7teO799fpvP6jrMq508M8c3Ryb25nPnVuZGVybGF5Q29sb3I8L3N0cm9uZz7K9NDUtqjS5bXju/fKsbXEsbO+sMmro6zNrMqxu7nT0DxzdHJvbmc+b25TaG93VW5kZXJsYXk8L3N0cm9uZz66zTxzdHJvbmc+b25IaWRlVW5kZXJsYXk8L3N0cm9uZz7Bvbj2yvTQ1L/J0tS84Mz9o6yxs76wyavP1Mq+us3S/rLYy7K85LXEysK8/qGjPC9wPg0KPHA+0OjSqtei0uK1xNK7tePKxzxzdHJvbmc+b25QcmVzczwvc3Ryb25nPsr00NSjrLHY0OvJ6NbDuPg8c3Ryb25nPlRvdWNoYWJsZUhpZ2hsaWdodDwvc3Ryb25nPqGjPC9wPg0KPHByZSBjbGFzcz0="brush:java;"> function onClick(){ var ToastAndroid = require('ToastAndroid') ToastAndroid.show('Awesome, Clicking!', ToastAndroid.SHORT); } class MyProject extends Component { render() { return ( Click Me! ); } }

\

4-3 透明觸摸效果

使用TouchableOpacity組件實現,點擊瞬間或者長按時,可以設定一個透明度視覺差,一般用於點擊圖片時使用。使用方式同TouchableHighlight。設定透明度的屬性是activeOpacity,如果不設置,默認值為0.2

function onClick(){
   var ToastAndroid = require('ToastAndroid')
   ToastAndroid.show('Awesome, Clicking!', ToastAndroid.SHORT);
}

class MyProject extends Component {
  render() {
    return (
      
        
            Click Me!
        
      
    );
  }
}

\

4-4 原生觸摸效果

使用TouchableNativeFeedback組件實現,點擊瞬間或者長按時,呈現原生系統的點擊效果。使用方式有點特殊,必須且只能包含一個節點,如果是Text這種多節點組件,必須在外面包一層View節點。而且這個功能目前並不完善,快速點擊時並不會出現原生點擊效果,只有較長時間按住時才正常。

function onClick(){
   var ToastAndroid = require('ToastAndroid')
   ToastAndroid.show('Awesome, Clicking!', ToastAndroid.SHORT);
}

class MyProject extends Component {
  render() {
    return (
      
        
            
                Click Me!
            
        
      
    );
  }
}

\

4-5 無反饋觸摸效果

使用TouchableWithoutFeedback組件實現,表示觸摸時無任何反饋效果(同4-1),使用方式同TouchableHighlight。facebook官方並不推薦使用這個組件,除非你有特殊的原因。

function onClick(){
   var ToastAndroid = require('ToastAndroid')
   ToastAndroid.show('Awesome, Clicking!', ToastAndroid.SHORT);
}

class MyProject extends Component {
  render() {
    return (
      
        
            Click Me!
        
      
    );
  }
}

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved