一.目標
首先我們來明確一下這次源碼解析的目標:
?1.鞏固上一篇《View的繪制(1)-setContentView源碼分析》的源碼機制.
?2.同時為下一篇《利用decorView機制實現底部彈出框》做準備.
二.SnackBar源碼分析
1.SnackBar的基本使用
1)只顯示文本:
Snackbar.make(view, "This is a message", Snackbar.LENGTH_LONG).show();
2)有點擊按鈕:
Snackbar.make(view, "This is a message", Snackbar.LENGTH_LONG)
.setAction("UNDO", new View.OnClickListener() {
@Override
public void onClick(View v) {
//TODO do something
}
})
.show();
這兩個就是SnackBar的基本使用,其他的使用方式可以查看文檔,在這里不是重點,最后我們放上一張上篇分析源碼得出的結論圖,在這里會用到,以此來鎮貼:
2.make 方法(注意:這里我的源代碼版本是android-25)
我們遵循一貫查看源碼的套路,從第一個使用到的方法make進入:
@NonNull
public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
@Duration int duration) {
Snackbar snackbar = new Snackbar(findSuitableParent(view));
snackbar.setText(text);
snackbar.setDuration(duration);
return snackbar;
}
方法很簡單,這里有個關鍵方法是findSuitableParent(view)【這個方法很重要!!!】,這個方法的參數是我們傳進來的視圖,那他的作用是啥呢?我們跟進這個方法瞅瞅:
private static ViewGroup findSuitableParent(View view) {
ViewGroup fallback = null;
do {
if (view instanceof CoordinatorLayout) {
//如果找到的父節點是CoordinatorLayout則返回這個父節點
// We've found a CoordinatorLayout, use it
return (ViewGroup) view;
} else if (view instanceof FrameLayout) {
//如果找到的id為content的framelayout節點則返回這個父節點
if (view.getId() == android.R.id.content) {
// If we've hit the decor content view, then we didn't find a CoL in the
// hierarchy, so use it.
return (ViewGroup) view;
} else {
//如果沒有找到任何的父節點則會用我們傳進來的視圖作為父節點
// It's not the content view but we'll use it as our fallback
fallback = (ViewGroup) view;
}
}
if (view != null) {
// Else, we will loop and crawl up the view hierarchy and try to find a parent
final ViewParent parent = view.getParent();
view = parent instanceof View ? (View) parent : null;
}
} while (view != null);//循環向上遍歷
return fallback;
}
這個方法里面的 if (view.getId() == android.R.id.content)用到的知識就是我們上次分析setContentView得出的結論,我們的視圖是放在id為Content的Framelayout中即如下圖,重要的事情貼兩遍:
到這里我們的父視圖已經找到,后面我們自己的視圖會添加到父視圖下面。然后我們跟進SnackBar的構造方法里。
3.SnackBar構造方法
構造函數不是很麻煩,我們直接貼代碼:
private Snackbar(ViewGroup parent) {
mTargetParent = parent;
mContext = parent.getContext();
//檢查主題
ThemeUtils.checkAppCompatTheme(mContext);
LayoutInflater inflater = LayoutInflater.from(mContext);
mView = (SnackbarLayout) inflater.inflate(
R.layout.design_layout_snackbar, mTargetParent, false);
//獲取無障礙輔助服務
mAccessibilityManager = (AccessibilityManager)
mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
}
我們看到源碼里面會先調用ThemeUtils.checkAppCompatTheme(mContext);來檢查主題,具體怎么檢查這里不深究。我們直接看到下面一句會inflate一個design_layout_snackbar的layout來得到SnackBarLayout(這里的inflate方法干了什么在上一篇setContentView源碼分析中有說過),那我們關注下兩個東西:
?1)design_layout_snackbar到底是啥樣的
<view xmlns:android="http://schemas.android.com/apk/res/android"
class="android.support.design.widget.Snackbar$SnackbarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"http://這個地方設置為bottom,所以我們的snackBar會在底部
style="@style/Widget.Design.Snackbar" />
我們看到view標簽里面有class="android.support.design.widget.Snackbar$SnackbarLayout"
說明這個view對應的布局就是SanckBarLayout,所以我們直接就看SnackBar的內部類SnackbarLayout是個啥:
?2)SnackBarLayout
public static class SnackbarLayout extends LinearLayout {
}
看到這里頓時豁然開朗,原來inflate的這個視圖是個LinearLayout呀。一萬只草泥馬奔騰而過.....
那接下來我們分部分來看SnackBarLayout的構造函數,看看這家伙干了些神馬事:
?2.1)第一部分是去獲取屬性,大家看代碼應該是老友了
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout);
mMaxWidth = a.getDimensionPixelSize(R.styleable.SnackbarLayout_android_maxWidth, -1);
mMaxInlineActionWidth = a.getDimensionPixelSize(
R.styleable.SnackbarLayout_maxActionInlineWidth, -1);
if (a.hasValue(R.styleable.SnackbarLayout_elevation)) {
ViewCompat.setElevation(this, a.getDimensionPixelSize(
R.styleable.SnackbarLayout_elevation, 0));
}
a.recycle();
//設置可點擊
setClickable(true);
2.2)然后就是我們的主要方法了,這里會去加載布局design_layout_snackbar_include布局
// Now inflate our content. We need to do this manually rather than using an <include>
// in the layout since older versions of the Android do not inflate includes with
// the correct Context.
//睜大眼睛認真看!!!!!!,這里加載了的layout作為linearlayout的布局
LayoutInflater.from(context).inflate(R.layout.design_layout_snackbar_include, this);
//底下省略一些代碼
..................
return insets;
}
});
所以我們順其自然地去看這個布局到底是何方神圣:
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/snackbar_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingTop="@dimen/design_snackbar_padding_vertical"
android:paddingBottom="@dimen/design_snackbar_padding_vertical"
android:paddingLeft="@dimen/design_snackbar_padding_horizontal"
android:paddingRight="@dimen/design_snackbar_padding_horizontal"
android:textAppearance="@style/TextAppearance.Design.Snackbar.Message"
android:maxLines="@integer/design_snackbar_text_max_lines"
android:layout_gravity="center_vertical|left|start"
android:ellipsize="end"/>
<Button
android:id="@+id/snackbar_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/design_snackbar_extra_spacing_horizontal"
android:layout_marginStart="@dimen/design_snackbar_extra_spacing_horizontal"
android:layout_gravity="center_vertical|right|end"
android:paddingTop="@dimen/design_snackbar_padding_vertical"
android:paddingBottom="@dimen/design_snackbar_padding_vertical" android:paddingLeft="@dimen/design_snackbar_padding_horizontal" android:paddingRight="@dimen/design_snackbar_padding_horizontal"
android:visibility="gone"
android:textColor="?attr/colorAccent"
style="?attr/borderlessButtonStyle"/>
</merge>
這個就是我們snackBar的主布局了,一個TextView一個Button,是不是到現在明白了為啥snackbar長那樣:
這里做個總結:我們的make方法會根據用戶傳進去的錨點view進行查找父視圖(CoordinateLayout或者id為content的framelayout),然后往父視圖添加SnackBarLayout這個LinearLayout.
4.show方法
現在我們分析完make方法,我們就繼續分析我們的show方法了。
public void show() {
SnackbarManager.getInstance().show(mDuration, mManagerCallback);
}
頭一熱,倒地休息五分鐘......這里怎么又蹦出SnackbarManager和mManagerCallback這個未知生物。What a fucking source code!!!!
吐槽完默默繼續,我們看下mManagerCallback是個什么東西:
final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
@Override
public void show() {
sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this));
}
@Override
public void dismiss(int event) {
sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, Snackbar.this));
}
};
原來這個是一個回調,顯示和隱藏,同時我們看到show和dismiss方法里面分別往Handler里面發送一個信息。我們直接跳到Handler里面看做了些啥動作:
sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_SHOW:
((Snackbar) message.obj).showView();
return true;
case MSG_DISMISS:
((Snackbar) message.obj).hideView(message.arg1);
return true;
}
return false;
}
});
我們看到Handler里面又調用了SnackBar類的showView和hideView方法,我們繼續轉到showView方法:
final void showView() {
//首先判斷SnackbarLayout沒有掛到其他的父視圖上面
if (mView.getParent() == null) {
final ViewGroup.LayoutParams lp = mView.getLayoutParams();
if (lp instanceof CoordinatorLayout.LayoutParams) {
// If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior
final CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp;
//新建一個Behavior,有用過MD庫的人都知道這個Behavior,主要是配合CoordinateLayout使用,在以后的文章會重點介紹
final Behavior behavior = new Behavior();
behavior.setStartAlphaSwipeDistance(0.1f);
behavior.setEndAlphaSwipeDistance(0.6f);
behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
//設置一個SwipeDismissBehavior,用來滑動刪除
behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
@Override
public void onDismiss(View view) {
view.setVisibility(View.GONE);
dispatchDismiss(Callback.DISMISS_EVENT_SWIPE);
}
@Override
public void onDragStateChanged(int state) {
switch (state) {
case SwipeDismissBehavior.STATE_DRAGGING:
case SwipeDismissBehavior.STATE_SETTLING:
// If the view is being dragged or settling, cancel the timeout
SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
break;
case SwipeDismissBehavior.STATE_IDLE:
// If the view has been released and is idle, restore the timeout
SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
break;
}
}
});
clp.setBehavior(behavior);
// Also set the inset edge so that views can dodge the snackbar correctly
clp.insetEdge = Gravity.BOTTOM;
}
//這個地方是重點mTargetParent就是我們剛才用錨點View查找到的父視圖
mTargetParent.addView(mView);
}
//省略一些代碼
.....................
if (ViewCompat.isLaidOut(mView)) {
if (shouldAnimate()) {
// If animations are enabled, animate it in
animateViewIn();
} else {
// Else if anims are disabled just call back now
onViewShown();
}
} else {
// Otherwise, add one of our layout change listeners and show it in when laid out
mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View view, int left, int top, int right, int bottom) {
mView.setOnLayoutChangeListener(null);
//判斷是否進行動畫顯示或者不需要
if (shouldAnimate()) {
// If animations are enabled, animate it in
animateViewIn();
} else {
// Else if anims are disabled just call back now
onViewShown();
}
}
});
}
}
到這里我們已經把我們的SnackBar顯示出來了,關鍵代碼就是將視圖添加進父視圖Id為content的FrameLayout里面或者是CoordinateLayout里面(mTargetParent.addView(mView);)。然后就會判斷需不需要有動畫效果顯示即 if (shouldAnimate()) {}.
5.SnackbarManager show方法
上面我們已經看完mManagerCallback 是啥了,我們是時候來看看SnackbarManager 的show方法了。首先我們看下SnackBarManager的getInstance():
static SnackbarManager getInstance() {
if (sSnackbarManager == null) {
sSnackbarManager = new SnackbarManager();
}
return sSnackbarManager;
}
其實就是個單例,我們就不去說明單例模式了,我們直接看show方法吧:
public void show(int duration, Callback callback) {
//這個地方加了個同步代碼塊
synchronized (mLock) {
//這個地方判斷是不是就是目前的SnackBar
if (isCurrentSnackbarLocked(callback)) {
// Means that the callback is already in the queue. We'll just update the duration
//如果要顯示的snackBar已經在顯示隊列里面則更新duration
mCurrentSnackbar.duration = duration;
// If this is the Snackbar currently being shown, call re-schedule it's
// timeout//移除Callback,避免內存泄露
mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
//);//重新關聯設置duration和Callback
scheduleTimeoutLocked(mCurrentSnackbar);
return;
} else if (isNextSnackbarLocked(callback)) {
// //判斷是否是接下來要顯示的Snackbar,是則更新duration
// We'll just update the duration
mNextSnackbar.duration = duration;
} else {
//不然就新創建一個記錄直接壓進隊列
// Else, we need to create a new record and queue it
mNextSnackbar = new SnackbarRecord(duration, callback);
}
if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,
Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {
//取消當前的snackbar顯示
// If we currently have a Snackbar, try and cancel it and wait in line
return;
} else {
// Clear out the current snackbar
mCurrentSnackbar = null;
// Otherwise, just show it now
//顯示我們的snackBar
showNextSnackbarLocked();
}
}
}
從顯示的代碼中可以知道當目前的mCurrentSnackbar不為空的話,則后面顯示的snackBar都會存儲在mNextSnackbar中,只有當當前顯示的Snackbar duration到了后,調用onDismissed方法,清空mCurrentSnackbar,然后才會顯示下一個Snackbar。其中onDismissed方法就是在cancelSnackbarLocked中調用的,源碼如下:
private boolean cancelSnackbarLocked(SnackbarRecord record, int event) {
final Callback callback = record.callback.get();
if (callback != null) {
// Make sure we remove any timeouts for the SnackbarRecord
mHandler.removeCallbacksAndMessages(record);
callback.dismiss(event);
return true;
}
return false;
}
dismiss完之后會把視圖從父視圖中刪除。如果當前的snackBar為空則就顯示我們新創建的snackBar:
private void showNextSnackbarLocked() {
if (mNextSnackbar != null) {
mCurrentSnackbar = mNextSnackbar;
mNextSnackbar = null;
final Callback callback = mCurrentSnackbar.callback.get();
if (callback != null) {
callback.show();
} else {
// The callback doesn't exist any more, clear out the Snackbar
mCurrentSnackbar = null;
}
}
}
到這里我們的snackBar源碼已經分析完成,希望在下一篇我們能找到感覺。