View系列:事件分发,android视频开发的面试问题

| MotionEvent.ACTION_MOVE | 滑动View |
| MotionEvent.ACTION_UP | 抬起View(与DOWN对应) |
| MotionEvent.ACTION_CANCEL | 结束事件 |
| MotionEvent.ACTION_OUTSIDE | 事件发生在视图范围外 |

辅助类

辅助类-dev

View触摸相关工具类全解

ViewConfiguration

获取 Android 系统常用的距离、速度、时间等常量

VelocityTracker

跟踪触摸事件的速度。此设置对于手势标准中包含速度的手势(例如滑动)非常有用。

GestureDetector

手势检测,该类支持的一些手势包括 onDown()、onLongPress()、onFling() 等。可以将 GestureDetector 与onTouchEvent() 方法结合使用。

OverScroller

回弹工具类,不同的回弹效果可以自定义不同的动画插值器

TouchDelegate

扩展子视图的可轻触区域

img```
view1.post(new Runnable() {
@Override
public void run() {
Rect bounds = new Rect();
// 获取View2占据的矩形区域在其父View(也就是View1)中的相对坐标
view2.getHitRect(bounds);
// 计算扩展后的矩形区域Bounds相对于View1的坐标
bounds.left -= 100;
bounds.top -= 50;
bounds.right += 100;
bounds.bottom += 50;
TouchDelegate touchDelegate = new TouchDelegate(bounds, view2);
// 为View1设置TouchDelegate
view1.setTouchDelegate(touchDelegate);
}
});


#### 事件处理

![image-20210531100411928](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3946d68b824e42c5abb7b154a4ec869e~tplv-k3u1fbpfcp-zoom-1.image)

*   每一个DOWN / MOVE / UP / CANCLE都是一个事件,并不是连起来才是一个事件
*   事件的消费,是看返回true/false,而不是看有没有处理操作
*   Activity、ViewGroup、View
    *   都有分发、消费事件的能力
    *   只有ViewGroup有拦截事件的能力

### 事件分发

> window中的View是树形结构,可能会重叠在一起,当我们点击的区域有多个View都可以响应的时候,事件分发机制决定了这个点击事件应该给谁处理。

分发机制类似洋葱模型、责任链模式、冒泡...

分发:Activity -> PhoneWindow -> DecorView -> ViewGroup -> @1 -> … -> View
消费:Activity <- PhoneWindow <- DecorView <- ViewGroup <- @1 <- … <- View


*   如果事件被消费,就意味着事件信息传递终止 如果在@1处消费事件,就不在往下传递了,直接返回
*   如果事件一直没有被消费,最后会传给Activity,如果Activity也不需要就被抛弃事

![image](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aa70dc7cc40241c6bf6664009cc85980~tplv-k3u1fbpfcp-zoom-1.image)

#### View

**优先级:**

1.  OnTouchListener.onTouch
2.  onTouchEven

注意:OnTouchListener.onTouch返回false,并不代表该View不消费事件了,得看dispatchTouchEvent返回的结果

public boolean dispatchTouchEvent(MotionEvent event) {

// 被遮盖,不响应事件
if (onFilterTouchEventForSecurity(event)) {

//setOnTouchListener设置的监听,优先级高
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}

        // 系统已实现好的,优先级低。
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    ...
    return result;
} 

**onTouchEvent:**

*   View即使设置了setEnable(false),只要是**可点击**状态就会消费事件,只是不做出回应
*   只要进入CLICKABLE判断,就返回true消费时间

| 事件 | 处理 |
| --- | --- |
| DOWN | 发送LongClick延迟消息,过期触发 |
| MOVE | 移除LongClick消息 |
| CANCLE | 移除LongClick消息 |
| UP | 移除LongClick消息  
触发Click事件 |

public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int action = event.getAction();

//View被禁用的话,如果是可以点击的,一样返回true,表示消费了事件。只是不作出回应。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
    return (((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}

// 委托:扩大点击事件、委托其它处理
if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}

/**
* 只要进入该if,就返回true,消费事件
*/
if (((viewFlags & CLICKABLE) == CLICKABLE ||
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
        (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            if (isInScrollingContainer) {
            } else {
                //长按事件,发送延时消息到队列
                checkForLongClick(0, x, y);
            }
            break;
        case MotionEvent.ACTION_MOVE:
            if (!pointInView(x, y, mTouchSlop)) {
                if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                    //移除长按事件的消息。
                    removeLongPressCallback();
                    setPressed(false);
                }
            }
            break;
        case MotionEvent.ACTION_UP:
            if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    // 移除长按事件的消息
                    removeLongPressCallback();

                    //点击事件: 可知onclick事件是在UP的时候触发
                    if (!focusTaken) {
                        if (!post(mPerformClick)) {
                            performClick();
                        }
                    }
                }
            }
            break;
        case MotionEvent.ACTION_CANCEL:
            //移除长按事件
            removeLongPressCallback();
            mHasPerformedLongPress = false;
            break;
    }
    return true;
}

return false;

}


#### ViewGroup

1.  DOWN事件:
    *   清除之前状态,mFirstTouchTarget = null
    *   进入逻辑1、2寻找接收事件的子View
        *   mFirstTouchTarget = null,进入逻辑3
        *   mFirstTouchTarget != null, 进入逻辑4
2.  MOVE/UP事件:
    *   mFirstTouchTarget = null,注释1处不满足逻辑1判断条件,进入逻辑3
    *   mFirstTouchTarget != null,不满足逻辑2判断条件,进入逻辑4
3.  CANCLE事件:
    *   mFirstTouchTarget = null,注释2处不满足逻辑1判断条件,进入逻辑3
    *   mFirstTouchTarget != null,注释2处不满足逻辑1判断条件,进入逻辑4

总结,

*   DOWN事件就是用来清理状态、寻找新接收事件子View的
    
*   DOWN事件的后续事件:
    
    *   未找到子View接收情况下,直接自己处理
    *   找到子View接收的情况下,直接给子View

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

// 如果该View被遮蔽,并且在被遮蔽时不响应点击事件,则不分发该触摸事件,即返回false。
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;

        /**
        * step1:DOWN事件的时候,表示最初开始事件,清除之前的状态。
        */
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 关键:每次DOWN的时候,清除前一个手势的mFirstTouchTarget = null
            cancelAndClearTouchTargets(ev);
            // 清除状态
            resetTouchState();
        }

        
        /**
        * step2:拦截判断
        */
        final boolean intercepted;
        // ACTION_DOWN(初始状态)或 有子View处理事件:判断是否拦截
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            //默认没有该标记位,返回false
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {//requestDisallowInterceptTouchEvent(false)
                //默认返回false,并不是每次都会调用
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {//requestDisallowInterceptTouchEvent(true)
                intercepted = false;
            }
        } else {
            //[注释1],没有子View接收事件,拦截
            intercepted = true;
        }


        /**
        * step3:找能接收事件的子View,并赋值给mFirstTouchTarget
        */
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL; //[注释2]
        // *****每次都会初始化这两个变量****
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        //如果在这一层不满足判断条件,直接就到[逻辑3,4]了。

//[逻辑1]
if (!canceled && !intercepted) {
View childWithAccessibilityFocus =
ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null;
/**
* 逻辑2的作用就是在最初的Down事件的时候,找到接收事件的子View,并各种赋值
* 如果没有找到接收事件的view,mFirstTouchTarget = null,
* 之后的事件就不用判断是否拦截(move/up,==null不满足if条件),
* 直接进入[逻辑3]给ViewGroup自己了。
*/
//[逻辑2]
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    for (int i = childrenCount - 1; i >= 0; i--) {//倒序
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);

                        // 记录接收事件的view链表中找该child,如果有的话就break,因为已经找到了
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            break;
                        }

                        //第2,3参数决定,进入后调用子View的dispatch方法。
                        if (dispatchTransformedTouchEvent(ev, false, child, 
                                                          idBitsToAssign)) {
                            // 能进入到这里,就说明child消费事件了
                            // addTouchTarget里给mFirstTouchTarget赋值,下面就会进入[逻辑4]。
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            // 因为已经消费了down事件,所以在[逻辑4],直接返回true。
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                    }
                }

                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event. 
                    // Assign the pointer to the least recently added target.
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }//[逻辑2] 
        }//[逻辑1] 

        /*
        step4:到这,已经跳出了上面的大嵌套判断!--上面的大嵌套就是用来找接收事件的子View的。     
          一旦确定找到了或者没有接收者,后面的事件:   
              1. 检查intercepte状态。     
              2. 进入下面的逻辑,后面的事件直接确定分发给谁
        */
        // 没有找到接收事件的View,以后的move/up也通过这一步给ViewGroup 

[逻辑3] if (mFirstTouchTarget == null) {
//没有接收事件的子View,调用自己的dispatchTouchEvent
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
[逻辑4] } else {//找到了接收事件的View
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 在DOWN找到接受事件的子View时,赋值alreadyDispatchedToNewTouchTarget = true
// 此时已经消费了事件,所以直接返回true
// 后面的其它事件中,alreadyDispatchedToNewTouchTarget被重置,不在满足该条件
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 判断是否 收到CANCEL事件 或 需要拦截事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 子View消费事件
//如果cancelChild为true,给子View发送cancle事件
[逻辑5] if (dispatchTransformedTouchEvent(ev, cancelChild,
get.child, target.pointerIdBits)) {
handled = true;
}
// 修改mFirstTouchTarget,使原来的子View不再接收事件
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
}
}
}
return handled;
}


#### Activity

> Touch事件先是传递到Activity,接着由Activity传递到最外层布局,然后一层层遍历循环到View

public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
// 交互 空实现
onUserInteraction();
}
// DecorView实际是ViewGroup的dispatchTouchEvent方法
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
// down点击到外部区域,消费事件,finish
return onTouchEvent(ev);
}


**onUserInteraction()**

这是一个空实现,用的也比较少,不深究: 此方法是activity的方法,当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法。下拉statubar、旋转屏幕、锁屏不会触发此方法。所以它会用在屏保应用上,因为当你触屏机器 就会立马触发一个事件,而这个事件又不太明确是什么,正好屏保满足此需求;或者对于一个Activity,控制多长时间没有用户点响应的时候,自己消失等。

**onTouchEvent(event)**

public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}


mWindow即使PhoneWindow,该方法是@hide,并且在Window类中定义。

/** @hide */
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {
return true;
}
return false;
}


*   mCloseOnTouchOutside是一个boolean变量,它是由Window的android:windowCloseOnTouchOutside属性值决定。
*   isOutOfBounds(context, event)是判断该event的坐标是否在context(对于本文来说就是当前的Activity)之外。是的话,返回true;否则,返回false。
*   peekDecorView()则是返回PhoneWindow的mDecor。

`总的来说:`如果设置了android:windowCloseOnTouchOutside为true,并且是DOWN事件点击了Activity外部区域(比如Activity是一个Dialog),返回true,消费事件,并且finish。

#### ACTION\_CANCEL

> 子View在接收事件过程中,被中断,父View会传给子View一个CANCEL事件

[逻辑4] } else {//找到了接收事件的View
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
} else {
// 判断是否 收到CANCEL事件 或 需要拦截事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted; //注释1

                     //如果cancelChild为true,给子View发送cancle事件

[逻辑5] if (dispatchTransformedTouchEvent(ev, cancelChild,
get.child, target.pointerIdBits)) {
handled = true;
}
// 修改mFirstTouchTarget,使原来的子View不再接收事件
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
//…
}
}
}

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            //发送CANCEL事件给子View
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    //...
} 

#### ACTION\_OUTSIDE

> 设置了FLAG\_WATCH\_OUTSIDE\_TOUCH,事件发生在当前视图的范围之外

例如,点击音量键之外的区域取消音量键显示:

//frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
// 给音量键Window设置FLAG_WATCH_OUTSIDE_TOUCH
mDialog = new CustomDialog(mContext);
mWindow = mDialog.getWindow();
mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH //设置Window Flag
| WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);

    // 重写onTouchEvent并处理ACTION_OUTSIDE事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mShowing) {
            if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
                dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE);
                return true;
            }
        }
        return false;
    } 

### 事件拦截

[一文解决Android View滑动冲突]( )

只有ViewGroup有事件拦截的能力,View可根据情况申请父View进行拦截

![image-20210531100411928](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/968bed8dec434b79adafa29e441ea9f9~tplv-k3u1fbpfcp-zoom-1.image)

#### View

View没有拦截事件的能力,只能根据不同需求调用mParent.requestDisallInterceptTouchEvent(true/false) 申请父View是否进行拦截。

`注意:`如果在子View接收事件的过程中被父View拦截,父View会给子View一个`CANCEL`事件,注意处理相关逻辑。

#### ViewGroup

##### onInterceptTouchEvent

*   设置了FLAG\_DISALLOW\_INTERCEPT标记时,不会调用
*   其它时候都会调用

/**
* ViewGroup事件分发时的拦截检查机制
*/
//默认没有该标记位,返回false
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//注释1
if (!disallowIntercept) {//requestDisallowInterceptTouchEvent(false)
intercepted = onInterceptTouchEvent(ev);//默认返回false
} else {
intercepted = false;//requestDisallowInterceptTouchEvent(true)
}

/**
* 默认返回false
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}

/*
* disallowIntercept = true时,不允许拦截,注释1为true
* disallowIntercept = false时,允许拦截,注释1为false
*/
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    // We're already in this state, assume our ancestors are too
    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        return;
    }

    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;// 添加标记,使得注释1为true
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;// 清除标记,使得注释1为false
    }

    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
} 

##### requestDisallowInterceptTouchEvent

*   true,不允许拦截,注释1为true,不会调用onInterceptTouchEvent
*   false,允许拦截,注释1为false(默认),调用onInterceptTouchEvent

`注意`:调用requestDisallowInterceptTouchEvent(false)申请拦截,**并不会真的就被父View拦截了**。它只是一个标记,使得父View会检查onInterceptTouchEvent这个方法(默认也会调用)。 它只会影响 `mGroupFlags & FLAG_DISALLOW_INTERCEPT`值,真正决定要不要被拦截是看 `onInterceptTouchEvent`的返回值。如果为true:

在注释1处`cancelChild = true`,会导致给子类发送`CANCEL`事件,然后修改`mFirstTouchTarget`,不再给子View传递事件。

[逻辑4] } else {//找到了接收事件的View
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
} else {
// 判断是否 收到CANCEL事件 或 需要拦截事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted; //注释1
// 子View消费事件
//如果cancelChild为true,给子View发送cancle事件
[逻辑5] if (dispatchTransformedTouchEvent(ev, cancelChild,
get.child, target.pointerIdBits)) {
handled = true;
}
// 修改mFirstTouchTarget,使原来的子View不再接收事件
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
}
}

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    //...
} 

#### Activity

Activity没有onInterceptTouchEvent方法,也没有mParent,不具备主动或被动拦截能力

### 滑动冲突

**常见场景:**

1.  内外层滑动方向不一致(如:ViewPager中嵌套竖向滑动的RecyclerView)
2.  内外层滑动方向一致(如:RecyclerView嵌套)

![image-20210602150942026](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e2b4d271d7bf49c983c97abd17eee445~tplv-k3u1fbpfcp-zoom-1.image)

一般从2个角度出发:父View自己主动拦截,或子View申请父View进行拦截

#### 父View

> 事件发送方,父View拦截。

父View根据自己的需求,选择在何时给onInterceptTouchEvent返回true,使事件直接分发给自己处理(前提:子View未设置requestDisallowInteceptTouchEvent(true),否则根本就不会经过onInterceptTouchEvent方法)。

*   DOWN不要拦截,否则根据事件分发逻辑,事件直接给父View自己处理了
*   UP不要拦截,否则子View无法出发click事件,无法移除longClick消息
*   在MOVE中根据逻辑需求判断是否拦截

public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (满足父容器的拦截要求) {
intercepted = true;
} else {

总结

其实客户端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

CodeChina开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》

Android大厂面试真题全套解析

2017-2020字节跳动Android面试真题解析PDF
()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (满足父容器的拦截要求) {
intercepted = true;
} else {

总结

其实客户端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

CodeChina开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》

[外链图片转存中…(img-bp6y9SCk-1630942524822)]

[外链图片转存中…(img-oCZHaHxF-1630942524823)]
然而Android架构学习进阶是一条漫长而艰苦的道路,不能靠一时激情,更不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值