2024年View系列:事件分发,Android程序员如何有效提升学习效率

最后

最后这里放上我这段时间复习的资料,这个资料也是偶然一位朋友分享给我的,里面包含了腾讯、字节跳动、阿里、百度2020-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。

还有 高级架构技术进阶脑图、高级进阶架构资料 帮助大家学习提升进阶,这里我也免费分享给大家也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

一起互勉~

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

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

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
  1. MOVE/UP事件:
  • mFirstTouchTarget = null,注释1处不满足逻辑1判断条件,进入逻辑3
  • mFirstTouchTarget != null,不满足逻辑2判断条件,进入逻辑4
  1. 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

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

一般从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 {
intercepted = false;
}
break;
}
}
return intercepted;
}

子View

事件接收方,内部拦截

事件已经传递到子View,子View只有选择是否消费该事件,或者向父View申请拦截事件。

注意:申请拦截事件,不代表就以后就收不到事件了。request只是会清除FLAG_DISALLOW_INTERCEPT标记,导致父View检查onInterceptTouchEvent方法,仅此而已(恢复到默认状态)。主要看父View.onInterceptTouchEvent中的返回值。

public boolean dispatchTouchEvent(MotionEvent event) {//或 onTouchEvent
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
parent.requestDisallowInterceptTouchEvent(true);//不许拦截
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此类点击事件) {
parent.requestDisallowInterceptTouchEvent(false);//申请拦截
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
}
return super.dispatchTouchEvent(event);
}

😢多点触控

安卓自定义View进阶-多点触控详解

自由地对图片进行缩放和移动

多点触控相关的事件:

事件简介
ACTION_DOWN第一个 手指 初次接触到屏幕 时触发。
ACTION_MOVE手指 在屏幕上滑动 时触发,会多次触发(单个或多个手指)。
ACTION_UP最后一个 手指 离开屏幕时触发。
ACTION_POINTER_DOWN有非主要的手指按下(即按下之前已经有手指在屏幕上)。
ACTION_POINTER_UP有非主要的手指抬起(即抬起之后仍然有手指在屏幕上)。
以下事件类型不推荐使用---以下事件在2.0开始,在 2.2 版本以上被废弃---
ACTION_POINTER_1_DOWN第 2 个手指按下,已废弃,不推荐使用。
ACTION_POINTER_2_DOWN第 3 个手指按下,已废弃,不推荐使用。
ACTION_POINTER_3_DOWN第 4 个手指按下,已废弃,不推荐使用。
ACTION_POINTER_1_UP第 2 个手指抬起,已废弃,不推荐使用。
ACTION_POINTER_2_UP第 3 个手指抬起,已废弃,不推荐使用。

尾声

开发是需要一定的基础的,我是08年开始进入Android这行的,在这期间经历了Android的鼎盛时期,和所谓的Android”凉了“。中间当然也有着,不可说的心酸,看着身边朋友,同事一个个转前端,换行业,其实当时我的心也有过犹豫,但是我还是坚持下来了,这次的疫情就是一个好的机会,大浪淘沙,优胜劣汰。再等等,说不定下一个黄金浪潮就被你等到了。

  • 330页 PDF Android核心笔记

  • 几十套阿里 、字节跳动、腾讯、华为、美团等公司2020年的面试题

  • PDF和思维脑图,包含知识脉络 + 诸多细节

  • Android进阶系统学习视频

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

id的鼎盛时期,和所谓的Android”凉了“。中间当然也有着,不可说的心酸,看着身边朋友,同事一个个转前端,换行业,其实当时我的心也有过犹豫,但是我还是坚持下来了,这次的疫情就是一个好的机会,大浪淘沙,优胜劣汰。再等等,说不定下一个黄金浪潮就被你等到了。

  • 330页 PDF Android核心笔记

[外链图片转存中…(img-IhLMTMEy-1715692988569)]

  • 几十套阿里 、字节跳动、腾讯、华为、美团等公司2020年的面试题

[外链图片转存中…(img-m71ERBKs-1715692988569)]

[外链图片转存中…(img-hBrZMeET-1715692988569)]

  • PDF和思维脑图,包含知识脉络 + 诸多细节

[外链图片转存中…(img-94rCsDKx-1715692988570)]

  • Android进阶系统学习视频

[外链图片转存中…(img-Ewu34A5s-1715692988570)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值