下面是 Android 中的 View 座標系的基本圖。要得到一個 View 的位置,咱們能夠藉助兩個對象,一個是 View ,一個是 MotionEvent。如下是它們的一些方法的位置的含義:git
文末有免費福利哦github
在 View 中共有 mLeft
, mRight
, mTop
和 mBottom
四個變量包含 View 的座標信息,你能夠在源碼中獲取它們的含義:面試
mLeft
:指定控件的左邊緣距離其父控件左邊緣的位置,單位:像素;mRight
:指定控件的右邊緣距離其父控件左邊緣的位置,單位:像素;mTop
:指定控件的上邊緣距離其父控件上邊緣的位置,單位:像素;mBottom
:指定控件的下邊緣距離其父控件上邊緣的位置,單位:像素。此外,View 中還有幾個方法用來獲取控件的位置等信息,實際上就是上面四個變量的 getter 方法:算法
getLeft()
:即 mLeft
;getRight()
:即 mRight
;getTop()
:即 mTop
;getBottom()
:即 mBottom
;因此,咱們能夠獲得兩個獲取 View 高度和寬度信息的方法:小程序
getHeight()
:即 mBottom - mTop
;getWidth()
:即 mRight - mLeft
;另外,就是 View 中的 getX()
和 getY()
兩個方法,你須要注意將其與 MotionEvent 中的同名方法進行區分。在沒有對控件進行平移的時候,getX()
與 getLeft()
返回結果相同,只是前者會在後者的基礎上加上平移的距離:性能優化
getX()
:即 mLeft + getTranslationX()
,即控件的左邊緣加上 X 方向平移的距離;getY()
:即 mTop + getTranslationY()
,即控件的上邊緣加上 Y 方向平移的距離;以上是咱們對 View 中獲取控件位置的方法的梳理,你能夠到源碼中查看它們更加相詳盡的定義,那更有助於本身的理解。架構
一般當你對控件進行觸摸監聽的時候會用到 MotionEvent ,它封住了觸摸的位置等信息。下面咱們對 MotionEvent 中的獲取點擊事件的位置的方法進行梳理,它主要涉及下面四個方法:ide
MotionEvent.getX()
:獲取點擊事件距離控件左邊緣的距離,單位:像素;MotionEvent.getY()
:獲取點擊事件距離控件上邊緣的距離,單位:像素;MotionEvent.getRawX()
:獲取點擊事件距離屏幕左邊緣的距離,單位:像素;MotionEvent.getRawY()
:獲取點擊事件距離屏幕上邊緣的距離,單位:像素。另外是觸摸事件中的三種典型的行爲,按下、移動和擡起。接下來的代碼示例中咱們會用到它們來判斷手指的行爲,並對其作響應的處理:源碼分析
MotionEvent.ACTION_DOWN
:按下的行爲;MotionEvent.ACTION_MOVE
:手指在屏幕上移動的行爲;MotionEvent.ACTION_UP
:手指擡起的行爲。咱們有幾種方式實現 View 的滑動:佈局
調用控件的 layout()
方法進行滑動,下面是該方法的定義:
public void layout(int l, int t, int r, int b) { /*...*/ } 複製代碼
其中的四個參數 l
, t
, r
, b
分別表示控件相對於父控件的左、上、右、下的距離,分別對應於上面的 mLeft
, mTop
, mRight
和 mBottom
。因此,調用該方法同時能夠改變控件的高度和寬度,但有時候咱們不須要改變控件的高度和寬度,只要移動其位置便可。因此,咱們又有方法 offsetLeftAndRight()
和 offsetTopAndBottom()
可使用,後者只會對控件的位置進行平移。所以,咱們能夠進行以下的代碼測試:
private int lastX, lastY; private void layoutMove(MotionEvent event) { int x = (int) event.getX(), y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: lastX = x; lastY = y; break; case MotionEvent.ACTION_MOVE: int offsetX = x - lastX, offsetY = y - lastY; getBinding().v.layout(getBinding().v.getLeft() + offsetX, getBinding().v.getTop() + offsetY, getBinding().v.getRight() + offsetX, getBinding().v.getBottom() + offsetY); break; case MotionEvent.ACTION_UP: break; } } 複製代碼
上面的代碼的效果是指定的控件會隨着手指的移動而移動。這裏咱們先記錄下按下的位置,而後手指移動的時候記錄下平移的位置,最後調用 layout()
便可。
文末有免費福利哦
上面已經提到過這兩個方法,它們只改變控件的位置,沒法改變大小。咱們只須要對上述代碼作少許修改就能夠實現一樣的效果:
getBinding().v.offsetLeftAndRight(offsetX); getBinding().v.offsetTopAndBottom(offsetY); 複製代碼
經過獲取並修改控件的 LayoutParams
,咱們同樣能夠達到修改控件的位置的目的。畢竟,自己這個對象就表明着控件的佈局:
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getBinding().v.getLayoutParams(); lp.leftMargin = getBinding().v.getLeft() + offsetX; lp.topMargin = getBinding().v.getTop() + offsetY; getBinding().v.setLayoutParams(lp); 複製代碼
使用動畫咱們也能夠實現控件移動的效果,這裏所謂的動畫主要是操做 View 的 transitionX
和 transitionY
屬性:
getBinding().v.animate().translationX(5f); getBinding().v.animate().translationY(5f); 複製代碼
關於動畫的內容,咱們會在後面詳細介紹。
scrollBy()
方法內部調用了 scrollTo()
,如下是這部分的源碼。scrollBy()
表示在當前的位置上面進行平移,而 scrollTo()
表示平移到指定的位置:
public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); } 複製代碼
一樣對上述代碼進行修改,咱們也能夠實現以前的效果:
((View) getBinding().v.getParent()).scrollBy(-offsetX, -offsetY); 複製代碼
或者
View parent = ((View) getBinding().v.getParent()); parent.scrollTo(parent.getScrollX()-offsetX, parent.getScrollY()-offsetY); 複製代碼
此外,還有一個須要注意的地方是:與上面的 offsetLeftAndRight()
和 offsetTopAndBottom()
不一樣的是,這裏咱們用了平移的值的相反數。緣由很簡單,由於咱們要使用這兩個方法的時候須要對指定的控件所在的父容器進行調用(正如上面是先獲取父控件)。當咱們但願控件相對於以前的位置向右下方向移動,就應該讓父容器相對於以前的位置向左上方向移動。由於實際上該控件相對於父控件的位置沒有發生變化,變化的是父控件的位置。(參考的座標系不一樣)
上面,咱們的測試代碼是讓指定的控件隨着手指移動,可是假如咱們但願控件從一個位置移動到另外一個位置呢?固然,它們也能夠實現,可是這幾乎就是在瞬間完成了整個操做,實際的UI效果確定不會好。因此,爲了讓滑動的過程看起來更加流暢,咱們能夠藉助 Scroller
來實現。
在使用 Scroller
以前,咱們須要先實例化一個 Scroller
:
private Scroller scroller = new Scroller(getContext()); 複製代碼
而後,咱們須要覆寫自定義控件的 computeScroll()
方法,這個方法會在繪製 View 的時候被調用。因此,這裏的含義就是,當 View 重繪的時候會調用 computeScroll()
方法,而 computeScroll()
方法會判斷是否須要繼續滾動,若是須要繼續滾動的時候就調用 invalidate()
方法,該方法會致使 View 進一步重繪。因此,也就是靠着這種不斷進行重繪的方式實現了滾動的效果。
滑動效果最終結束的判斷是經過 Scroller
的 computeScrollOffset()
方法實現的,當滾動中止的時候,該方法就會返回 false
,這樣不會繼續調用 invalidate()
方法,於是也就不會繼續繪製了。下面是該方法典型的覆寫方式:
@Override public void computeScroll() { super.computeScroll(); if (scroller.computeScrollOffset()) { ((View) getParent()).scrollTo(scroller.getCurrX(), scroller.getCurrY()); invalidate(); } } 複製代碼
而後,咱們再加入一個滾動到指定位置的方法,在該方法內部咱們使用了 2000ms 來指定完成整個滑動所須要的時間:
public void smoothScrollTo(int descX, int descY) { scroller.startScroll(getScrollX(), getScrollY(), descX - getScrollX(), descY - getScrollY(), 2000); invalidate(); } 複製代碼
這樣定義了以後,咱們只須要在須要滾動的時候調用自定義 View 的 smoothScrollTo()
方法便可。
在類 ViewConfiguration
中定義了一些列的常量用來標誌指定的行爲,好比,TouchSlop
就是滑動的最小的距離。你能夠經過 ViewConfiguration.get(context)
來獲取 ViewConfiguration
實例,而後經過它的 getter 方法來獲取這些常量的定義。
VelocityTracker
用來檢測手指滑動的速率,它的使用很是簡單。在使用以前,咱們先使用它的靜態方法 obtain()
獲取一個實例,而後在 onTouch()
方法中調用它的 addMovement(MotionEvent)
方法:
velocityTracker = VelocityTracker.obtain(); 複製代碼
隨後,當咱們想要得到速率的時候,先調用 computeCurrentVelocity(int)
傳入一個時間片斷,單位是毫秒,而後調用 getXVelocity()
和 getYVelocity()
分別得到在水平和豎直方向上的速率便可:
velocityTracker.computeCurrentVelocity((int) duration); getBinding().tvVelocity.setText("X:" + velocityTracker.getXVelocity() + "\n" + "Y:" + velocityTracker.getYVelocity()); 複製代碼
本質上,計算速率的時候是用指定時間的長度變化除以咱們傳入的時間片。當咱們使用完了 VelocityTracker
以後,須要回收資源:
velocityTracker.clear(); velocityTracker.recycle(); 複製代碼
GestureDectector
用來檢測手指的手勢。在使用它以前咱們須要先獲取一個 GestureDetector
的實例:
mGestureDetector = new GestureDetector(getContext(), new MyOnGestureListener()); 複製代碼
這裏咱們用了 GestureDetector
的構造方法,須要傳入一個 OnGestureListener
對象。這裏咱們用了 MyOnGestureListener
實例。 MyOnGestureListener
是一個自定義的類,實現了 OnGestureListener
接口:
private class MyOnGestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onSingleTapUp(MotionEvent e) { ToastUtils.makeToast("Click detected"); return false; } @Override public void onLongPress(MotionEvent e) { LogUtils.d("Long press detected"); } @Override public boolean onDoubleTap(MotionEvent e) { LogUtils.d("Double tab detected"); return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { LogUtils.d("Fling detected"); return true; } } 複製代碼
在 MyOnGestureListener
中,咱們覆寫了它的一些方法。好比,單擊、雙擊和長按等等,當檢測到相應的手勢的時候這些方法就會被調用。
而後,咱們能夠這樣使用 GestureDetector
,只要在控件的觸摸事件回調中調用便可:
getBinding().vg.setOnTouchListener((v, event) -> { mGestureDetector.onTouchEvent(event); return true; }); 複製代碼
當討論事件分發機制的時候,咱們首先要了解 Android 中 View
的組成結構。在 Android 中,一個 Activity 包含一個 PhoneWindow
,當咱們在 Activity 中調用 setContentView()
方法的時候,會調用該 PhoneWindow
的 setContentView()
方法,並在這個方法中生成一個 DecorView
做爲 Activity 的跟 View
。
根據上面的分析,當一個點擊事件被觸發的時候,首先接收到該事件的是 Activity
。由於,Activity
覆蓋了整個屏幕,咱們須要先讓它接收事件,而後它把事件傳遞給根 View
以後,再由根 View
向下繼續傳遞。這樣不斷縮小搜索的範圍,直到最頂層的 View
。固然,任何的父容器均可以決定這個事件是否是要繼續向下傳遞,所以,咱們能夠大體獲得下面這個事件傳遞的圖:
左邊的圖是一個 Activity 內部的 View
和 Window
的組織結構。右面的圖能夠看做它的切面圖,其中的黑色箭頭表示事件的傳遞過程。這裏事件傳遞的過程是先從下到上,而後再從上到下。也就是從大到小,不判定位到觸摸的控件,其中每一個父容器能夠決定是否將事件傳遞下去。(須要注意的地方是,若是一個父容器有多個子元素的話,那麼在這些子元素中進行遍歷的時候,順序是從上往下的,也就是按照展現的順序)。
上面咱們分析了 Android 事件傳遞的過程,相信你有了一個大體的瞭解。可是,想要了解整個事件傳遞過程具體涉及了哪些方法、如何做用等,還須要咱們對源碼進行分析。
文末有免費福利哦
當觸摸事件發生的時候,首先會被 Activity 接收到,而後該 Activity 會經過其內部的 dispatchTouchEvent(MotionEvent)
將事件傳遞給內部的 PhoneWindow
;接着 PhoneWindow
會把事件交給 DecorView
,再由 DecorView
交給根 ViewGroup
。剩下的事件傳遞就只在 ViewGroup
和 View
之間進行。咱們能夠經過覆寫 Activity 的 dispatchTouchEvent(MotionEvent)
來阻止把事件傳遞給 PhoneWindow
。實際上,在咱們開發的時候不會對 Window
的事件傳遞方法進行重寫,通常是對 ViewGroup
或者 View
。因此,下面咱們的分析只在這兩種控件之間進行。
當討論 View 的事件分發機制的時候,無外乎下面三個方法:
boolean onInterceptTouchEvent(MotionEvent ev)
:用來對事件進行攔截,該方法只存在於 ViewGroup 中。通常咱們會經過覆寫該方法來攔截觸摸事件,使其再也不繼續傳遞給子 View。boolean dispatchTouchEvent(MotionEvent event)
:用來分發觸摸事件,通常咱們不覆寫該方法,返回 true
則表示事件被處理了。在 View 中,它負責根據手勢的類型和控件的狀態對事件進行處理,會回調咱們的 OnTouchListener
或者 OnClickListener
;在 ViewGroup 中,該方法被覆寫,它的責任是對事件進行分發,會對全部的子 View 進行遍歷,決定是否將事件分發給指定的 View。boolean onTouchEvent(MotionEvent event)
:用於處理觸摸事件,返回 true
表示觸摸事件被處理了。ViewGroup 沒有覆寫該方法,故在 ViewGroup 中與 View 中的功能是同樣的。須要注意的是,若是咱們爲控件設置了 OnTouchListener
而且在或者中返回了 true
,那麼這個方法不會被調用,也就是 OnTouchListener
比該方法的優先級較高。對咱們開發來講,就是 OnTouchListener
比 OnClickListener
和 OnLongClickListener
的優先級要高。因而,咱們能夠獲得以下的僞代碼。這段代碼是存在於 ViewGroup 中的,也就是事件分發機制的核心代碼:
boolean dispatchTouchEvent(MotionEvent e) { boolean result; if (onInterceptTouchEvent(e)) { result = super.dispatchTouchEvent(e); } else { result = child.dispatchTouchEvent(e); } return result; } 複製代碼
按照上述分析,觸摸事件通過 Activity 傳遞給根 ViewGroup 以後:
若是 ViewGourp 覆寫了 onInterceptTouchEvent()
而且返回了 true
就表示但願攔截該方法,因而就把觸摸事件交給當前 ViewGroup 進行處理(觸發 OnTouchListener
或者 OnClickListener
等);不然,會交給子元素的繼續分發。若是該子元素是 ViewGroup 的話,就會在該子 View 中執行一遍上述邏輯,不然會在當前的子元素中對事件進行處理(觸發 OnTouchListener
或者 OnClickListener
等)……就這樣一層層地遍歷下去,本質上是一個深度優先的搜索算法。
這裏咱們對整個事件分發機制的總體作了一個素描,在接下來的文章中咱們會對各個方法的細節進行源碼分析,爲了防止您在接下來的行文中迷路,咱們先把這個總體邏輯按下圖進行描述:
上述咱們分析了事件分發機制的原理,下面咱們經過源代碼來更具體地瞭解這塊是如何設計的。一樣,咱們的焦點也只在那三個須要重點關注的方法。
首先,咱們來看 ViewGroup 中的 dispatchTouchEvent(MotionEvent)
方法,咱們節選了其一部分:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { // ... boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; if (actionMasked == MotionEvent.ACTION_DOWN) { // 1 // 這裏表示若是是一個新的觸摸事件就要重置全部的狀態,其中包括將 mFirstTouchTarget 置爲 null cancelAndClearTouchTargets(ev); resetTouchState(); } // 在這裏檢查是否攔截了事件,mFirstTouchTarget 是以前處理觸摸事件的 View 的封裝 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 這裏判斷該 ViewGroup 是否禁用了攔截,由 requestDisallowInterceptTouchEvent 設置 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); } else { intercepted = false; } } else { // 非按下事件而且 mFirstTouchTarget 爲 null,說明判斷過攔截的邏輯而且啓用了攔截 intercepted = true; } // ... } // ... return handled; } 複製代碼
上面代碼是咱們節選的 ViewGroup 攔截事件的部分代碼,這裏的邏輯顯然比僞代碼複雜的多。不過,儘管如此,這些代碼確實必不可少的。由於,當咱們要去判斷是否攔截一個觸摸事件的時候,此時觸摸的事件仍然在繼續,這意味着這個方法會被持續調用;擡起的時候再按下,又是另外一次調用。考慮到這個連續性,咱們須要多作一些邏輯。
這裏咱們首先在 1 處經過行爲是不是「按下」的來判斷是不是一次新的觸摸事件,若是是的話咱們須要重置當前的觸摸狀態。其次,咱們須要根據事件的類型來決定是否應該調用 onInterceptTouchEvent()
,由於對一次觸摸事件,咱們只須要在「按下」的時候判斷一次就夠了。因此,顯然咱們須要將 MotionEvent.ACTION_DOWN
做爲一個判斷條件。而後,咱們使用 mFirstTouchTarget
這個全局的變量來記錄上次攔截的結果——若是以前的事件交給過子元素處理,那麼它就不爲空。
除了 mFirstTouchTarget
,咱們還須要用 mGroupFlags
的 FLAG_DISALLOW_INTERCEPT
標誌位來判斷該 ViewGroup 是否禁用了攔截。這個標誌位能夠經過 ViewGroup 的 requestDisallowInterceptTouchEvent(boolean)
來設置。只有沒有禁用攔截事件的時候咱們才須要調用 onInterceptTouchEvent()
判斷是否開啓了攔截。
若是在上面的操做中事件沒有被攔截而且沒有被取消,那麼就會進入下面的邏輯。這部分代碼處在 dispatchTouchEvent()
中。在下面的邏輯中會根據子元素的狀態將事件傳遞給子元素:
// 對子元素進行倒序遍歷,即從上到下進行遍歷 final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); // ... // 判斷子元素是否能接收觸摸事件:能接收事件而且不是正在進行動畫的狀態 if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } // ... // 在這裏調用了 dispatchTransformedTouchEvent() 方法將事件傳遞給子元素 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // ... 記錄一些狀態信息 // 在這裏完成對 mFirstTouchTarget 的賦值,表示觸摸事件被子元素處理 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; // 結束循環,完成子元素的遍歷 break; } // 顯然,若是到了這一步,那麼子元素的遍歷仍將繼續 } 複製代碼
當判斷了指定的 View 能夠接收觸摸事件以後會調用 dispatchTransformedTouchEvent()
方法分發事件。其定義的節選以下:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; // ... if (child == null) { // 本質上邏輯與 View 的 dispatchTouchEvent() 一致 handled = super.dispatchTouchEvent(transformedEvent); } else { // ... // 交給子元素繼續分發事件 handled = child.dispatchTouchEvent(transformedEvent); } return handled; } 複製代碼
dispatchTransformedTouchEvent()
會根據傳入的 child
是否爲 null
分紅兩種調用的情形:事件沒有被攔截的時候,讓子元素繼續分發事件;另外一種是當事件被攔截的時候,調用當前的 ViewGroup 的 super.dispatchTouchEvent(transformedEvent)
處理事件。
上面咱們分析的 dispatchTouchEvent(MotionEvent)
是 ViewGroup 中重寫以後的方法。可是,正如咱們上面的分析,重寫以前的方法老是會被調用,只是對象不一樣。這裏咱們就來分析如下這個方法的做用。
public boolean dispatchTouchEvent(MotionEvent event) { // ... boolean result = false; // .... if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } // 這裏回調了 setOnTouchListener() 方法傳入的 OnTouchListener ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } // 若是 OnTouchListener 沒有被回調過或者返回了 false,就會調用 onTouchEvent() 進行處理 if (!result && onTouchEvent(event)) { result = true; } } // ... return result; } 複製代碼
根據上面的源碼分析,咱們知道,若是當前的 View 設置過 OnTouchListener
, 而且在 onTouch()
回調方法中返回了 true
,那麼 onTouchEvent(MotionEvent)
將不會獲得調用。那麼,咱們再來看一下 onTouchEvent()
方法:
public boolean onTouchEvent(MotionEvent event) { // ... // 判斷當前控件是不是能夠點擊的:實現了點擊、長按或者設置了可點擊屬性 final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // ... if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: // ... if (!focusTaken) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } } // ... break; case MotionEvent.ACTION_DOWN: // ... if (!clickable) { checkForLongClick(0, x, y); break; } // ... break; // ... } return true; } return false; } 複製代碼
這裏先判斷指定的控件是不是可點擊的,便是否設置過點擊或者長按的事件。而後會在手勢擡起的時候調用 performClick()
方法,並會在這個方法中嘗試從 ListenerInfo
取 OnClickListener
進行回調;會在長按的時候進行監聽以調用相應長按事件;其餘的事件與之相似,能夠自行分析。因此,咱們能夠得出結論:當爲控件的觸摸事件進行了賦值而且在其中返回了 true
就表明該事件被消費了,即便設置過單擊和長按事件也不會被回調,觸摸事件的優先級比後面二者要高。
通過上述分析,咱們能夠知道 View 中的 dispatchTouchEvent(MotionEvent)
方法就是用來對手勢進行處理的,因此回到 4.3.2
,那裏的意思就是:若是 ViewGroup 攔截了觸摸事件,那麼它就本身來對事件進行處理;不然就把觸摸事件傳遞給子元素,讓它來進行處理。
以上就是咱們對 Android 中事件分發機制的詳解,你能夠經過圖片和代碼結合來更透徹得了解這方面的內容。雖然這部分代碼比較多、比較長,可是每一個地方的設計都是合情合理的。
你能夠在Github獲取以上程序的源代碼: Android-references。
想學習更多Android知識,請加入Android技術開發企鵝交流 7520 16839
進羣與大牛們一塊兒討論,還可獲取Android高級架構資料、源碼、筆記、視頻
包括 高級UI、Gradle、RxJava、小程序、Hybrid、移動架構、React Native、性能優化等全面的Android高級實踐技術講解性能優化架構思惟導圖,和BATJ面試題及答案!
羣裏免費分享給有須要的朋友,但願可以幫助一些在這個行業發展迷茫的,或者想系統深刻提高以及困於瓶頸的朋友,在網上博客論壇等地方少花些時間找資料,把有限的時間,真正花在學習上,因此我在這免費分享一些架構資料及給你們。但願在這些資料中都有你須要的內容。