React-Native系列Android——Touch事件原理及状态效果
2018-01-24 15:05
507 查看
Native原生相比于Hybrid或H5最大长处是具有流畅和复杂的交互效果,触摸事件便是当中重要一项,包括点击(Click)、长按(LongClick)、手势(gesture)等。
以最简单常见的点击(Click)为例,Native组件能够自己定义selector。使得被点击的组件具有动态效果,Android 5.0以上甚至能够有涟漪效果(Material Design)。而这些在Hybrid或H5中非常难实现。非常多时候区分它们与原生最简单的方法就是检验点击交互效果。
React-Native的强大之处在于实现了较为全面的Touch事件机制,尽管仍略有缺陷,但相比于Hybrid或H5的体验而言,已经足足提高了一大截。下面分析解说一下事实上现原理,和详细使用方式。
1、Touch事件机制
假设阅读过React-Native源代码的话。应该了解React-Native页面的UI根视图是ReactRootView,包路径是:com.facebook.react.ReactRootView,它是FramLayout的一个子类。
首先,来看一下ReactActivity这个页面基类。ReactRootView是怎样作为React-Native的根视图被初始化及加入的。
在ReactActivity的onCreate这个生命周期里,直接实列化,然后作为当前Window的ContentView,也就能够觉得其是全部React-Native组件的根视图。
熟悉Android触摸事件机制的,应该知道视图树中。触摸事件是逐级传递的。每一个视图(View)中有两个接收和处理Touch事件的方法。各自是onInterceptTouchEvent和onTouchEvent,这两个方法的差别为:
onInterceptTouchEvent的传递层级是由父视图向子视图,顾名思义,通经常使用作事件拦截。
onTouchEvent的传递层级是由子视图向父视图,通经常使用作事件处理。
我们来看一下ReactRootView的事件接收和处理。
非常明显,这里onInterceptTouchEvent和onTouchEvent的处理都是全部交给handleTouchEvent方法统一处理的。
我们再继续看一下handleTouchEvent方法。
代码不是非常多。也非常好理解。
先来看一下凝视,意思是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 1是ReactViewGroup类型。同上方1过程处理;假设child 1是非ReactViewGroup类型,即ImageView、TextView等非复合型视图,推断其自身是否消费Touch事件,一般除具有Span属性的TextView外。基本都是消费Touch事件的。假设消费Touch事件。返回child 1,假设不消费Touch事件。返回null。
3、child 2,假设2中child 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中。
详细有三层逻辑:findTouchTargetView、findClosestReactAncestor、getTouchTargetForView。终于是要返回目标View的ID。代码我们一一来看。
1、findTouchTargetView
循环遍历ReactRootView 的视图树,通过isTransformedTouchPointInView方法推断触摸点坐标是否位于当前遍历子视图的区域内。有一点须要特别注意,就是坐标的处理。默认的触摸点坐标是基于ReactRootView 的坐标系參照,假设遍历到子视图,须要将触摸点坐标转换成以子视图为坐标系參照的坐标。主要是通过上方代码中的childPoint变量保存和处理的。
触摸点坐标是否位于当前遍历子视图的区域内,通过findTouchTargetViewWithPointerEvents方法。推断当前遍历子视图是否消费Touch事件。
findTouchTargetViewWithPointerEvents方法对PointerEvents的四种枚举做了相应处理。NONE返回null。BOX_ONLY返回当前视图,BOX_NONE和AUTO继续遍历,递归调用了findTouchTargetView。
2、findClosestReactAncestor
因为查找终于是要返回目标视图的ID,假设目标视图的ID非法小于0,则返回其父视图作为替代。此处作用不是非常理解,忘解答。感激不尽。
3、getTouchTargetForView
这种方法是针对ReactTextView做特殊处理的。因为ReactTextView中可能存在消费Touch事件的Span,假设有则返回其Span的tag值(详细请阅读ReactTextView和ReactTagSpan)。
3、Touch事件发送
代码位于com.facebook.react.uimanager.events.EventDispatcher中,先来看一下EventDispatcher对象的初始化。
EventDispatcher实现了LifecycleEventListener接口,在ReactActivity的各个生命周期执行时回调给EventDispatcher。
onHostResume方法相应Activity的onResume生命周期。主要通过ReactChoreographer单例来post了一个ScheduleDispatchFrameCallback。而ReactChoreographer是对Choreographer的一层封装,这里能够直接看成是Choreographer。Choreographer是一个消息处理器,详细作用不多讲,请參考http://blog.csdn.net/farmer_cc/article/details/18619429 。
ScheduleDispatchFrameCallback是EventDispatcher的一个内部Choreographer.FrameCallback实现类。接下来看看ScheduleDispatchFrameCallback这个回调类里面处理了哪些东西。
在doFrame的回调方法里面一共做了两件事情:
1、将mDispatchEventsRunnable塞进Javascript线程处理队列中,此线程队列内部有一个Looper消息队列并持有当前线程的Handler消息句柄,终于mDispatchEventsRunnable是通过此Handler塞进消息队列完毕处理的。
2、递归调用,再次post了当前ScheduleDispatchFrameCallback。达到一个循环的目的。
接下里,我们看看mDispatchEventsRunnable是怎样发送Touch事件的。
DispatchEventsRunnable这个对象的作用,仅仅有一个:循环遍历mEventsToDispatch数组,然后调用event的dispatch方法发送给Javascript。
这里涉及到RCTEventEmitter的一个JS组件类,里面有一个receiveTouches(String eventName, WritableArray touches, WritableArray changedIndices)的方法用来与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属性实现。
4-2 变色触摸效果
使用TouchableHighlight组件实现,点击瞬间或者长按时,能够设定一个颜色视觉差。TouchableHighlight标签必须包裹被点击组件。使用underlayColor属性定义点击时的背景色,同一时候还有onShowUnderlay和onHideUnderlay两个属性能够监听,背景色显示和隐藏瞬间的事件。
须要注意的一点是onPress属性,必须设置给TouchableHighlight。
4-3 透明触摸效果
使用TouchableOpacity组件实现,点击瞬间或者长按时。能够设定一个透明度视觉差。一般用于点击图片时使用。使用方式同TouchableHighlight。设定透明度的属性是activeOpacity,假设不设置。默认值为0.2。
4-4 原生触摸效果
使用TouchableNativeFeedback组件实现,点击瞬间或者长按时,呈现原生系统的点击效果。使用方式有点特殊,必须且仅仅能包括一个节点,假设是Text这样的多节点组件,必须在外面包一层View节点。并且这个功能眼下并不完好,高速点击时并不会出现原生点击效果,仅仅有较长时间按住时才正常。
4-5 无反馈触摸效果
使用TouchableWithoutFeedback组件实现,表示触摸时无不论什么反馈效果(同4-1),使用方式同TouchableHighlight。facebook官方并不推荐使用这个组件,除非你有特殊的原因。
本博客不定期持续更新。欢迎关注和交流:
http://blog.csdn.net/megatronkings
以最简单常见的点击(Click)为例,Native组件能够自己定义selector。使得被点击的组件具有动态效果,Android 5.0以上甚至能够有涟漪效果(Material Design)。而这些在Hybrid或H5中非常难实现。非常多时候区分它们与原生最简单的方法就是检验点击交互效果。
React-Native的强大之处在于实现了较为全面的Touch事件机制,尽管仍略有缺陷,但相比于Hybrid或H5的体验而言,已经足足提高了一大截。下面分析解说一下事实上现原理,和详细使用方式。
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); } ... }
在ReactActivity的onCreate这个生命周期里,直接实列化,然后作为当前Window的ContentView,也就能够觉得其是全部React-Native组件的根视图。
熟悉Android触摸事件机制的,应该知道视图树中。触摸事件是逐级传递的。每一个视图(View)中有两个接收和处理Touch事件的方法。各自是onInterceptTouchEvent和onTouchEvent,这两个方法的差别为:
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; } ... }
非常明显,这里onInterceptTouchEvent和onTouchEvent的处理都是全部交给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 1是ReactViewGroup类型。同上方1过程处理;假设child 1是非ReactViewGroup类型,即ImageView、TextView等非复合型视图,推断其自身是否消费Touch事件,一般除具有Span属性的TextView外。基本都是消费Touch事件的。假设消费Touch事件。返回child 1,假设不消费Touch事件。返回null。
3、child 2,假设2中child 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; } ... }
详细有三层逻辑:findTouchTargetView、findClosestReactAncestor、getTouchTargetForView。终于是要返回目标View的ID。代码我们一一来看。
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返回null。BOX_ONLY返回当前视图,BOX_NONE和AUTO继续遍历,递归调用了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,假设有则返回其Span的tag值(详细请阅读ReactTextView和ReactTagSpan)。
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方法相应Activity的onResume生命周期。主要通过ReactChoreographer单例来post了一个ScheduleDispatchFrameCallback。而ReactChoreographer是对Choreographer的一层封装,这里能够直接看成是Choreographer。Choreographer是一个消息处理器,详细作用不多讲,请參考http://blog.csdn.net/farmer_cc/article/details/18619429 。
ScheduleDispatchFrameCallback是EventDispatcher的一个内部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线程处理队列中,此线程队列内部有一个Looper消息队列并持有当前线程的Handler消息句柄,终于mDispatchEventsRunnable是通过此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数组,然后调用event的dispatch方法发送给Javascript。
这里涉及到RCTEventEmitter的一个JS组件类,里面有一个receiveTouches(String eventName, WritableArray touches, WritableArray changedIndices)的方法用来与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 ( <View style={styles.container}> <Text onPress={onClick}>Click Me!</Text> </View> ); } }
4-2 变色触摸效果
使用TouchableHighlight组件实现,点击瞬间或者长按时,能够设定一个颜色视觉差。TouchableHighlight标签必须包裹被点击组件。使用underlayColor属性定义点击时的背景色,同一时候还有onShowUnderlay和onHideUnderlay两个属性能够监听,背景色显示和隐藏瞬间的事件。
须要注意的一点是onPress属性,必须设置给TouchableHighlight。
function onClick(){ var ToastAndroid = require('ToastAndroid') ToastAndroid.show('Awesome, Clicking!', ToastAndroid.SHORT); } class MyProject extends Component { render() { return ( <View style={styles.container}> <TouchableHighlight underlayColor='gray' onPress={onClick}> <Text>Click Me!</Text> </TouchableHighlight> </View> ); } }
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 ( <View style={styles.container}> <TouchableOpacity activeOpacity={0.3} onPress={onClick}> <Text>Click Me!</Text> </TouchableOpacity> </View> ); } }
4-4 原生触摸效果
使用TouchableNativeFeedback组件实现,点击瞬间或者长按时,呈现原生系统的点击效果。使用方式有点特殊,必须且仅仅能包括一个节点,假设是Text这样的多节点组件,必须在外面包一层View节点。并且这个功能眼下并不完好,高速点击时并不会出现原生点击效果,仅仅有较长时间按住时才正常。
function onClick(){ var ToastAndroid = require('ToastAndroid') ToastAndroid.show('Awesome, Clicking!', ToastAndroid.SHORT); } class MyProject extends Component { render() { return ( <View style={styles.container}> <TouchableNativeFeedback onPress={onClick}> <View> <Text>Click Me!</Text> </View> </TouchableNativeFeedback> </View> ); } }
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 ( <View style={styles.container}> <TouchableWithoutFeedback onPress={onClick}> <Text>Click Me!</Text> </TouchableWithoutFeedback> </View> ); } }
本博客不定期持续更新。欢迎关注和交流:
http://blog.csdn.net/megatronkings
相关文章推荐
- React-Native系列Android——Touch事件原理及状态效果
- React-Native系列Android——Touch事件原理及状态效果
- React-Native系列Android——Native与Javascript通信原理(三)
- React-Native系列Android——Native与Javascript通信原理(一)
- 《React-Native系列》23、 js实现下拉刷新效果(Android和iOS通用)
- React-Native系列Android——Native与Javascript通信原理(二)
- React-Native系列Android——Native与Javascript通信原理(一)
- React-Native系列Android——Native与Javascript通信原理(一)
- react native 实战系列教程之热更新原理分析与实现
- Android事件处理机制系列-----------Touch事件处理机制
- 第21天 Android Touch事件学习 8 事件分发原理
- React-Native系列Android——SoLoader加载动态链接库
- 《React-Native系列》19、 ListView组件之上拉刷新(iOS和Android通用)
- React-Native系列Android——Win7系统环境搭建问题汇总
- React Native Android ScrollView 去除阴影效果
- React-Native爬过的坑,解决ListView 在Android手机上无吸顶效果
- reactnative LayoutAnimation 在android中没效果
- Android Touch事件原理加实例分析
- android自定义控件系列教程-----touch事件的传递
- Android React-Native系列之<一>零基础搭建React-Native开发环境