标签:
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的根视图被初始化及添加的。
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处理线程的队列中,其内部是通过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组件类,里面有一个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 (
<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>
);
}
}
React-Native系列Android——Touch事件原理及状态效果
标签:
原文地址:http://blog.csdn.net/megatronkings/article/details/51069333