Snackbar是谷歌集成的design包下的一个用于消息提示的控件,介于Dialog和Toast之间,结合了两者的优点,又解决了Dialog强提示和Toast无法交互的缺点,使用方法也很简单,首先我们需要引入design包,然后在代码中调用
代码语言:javascript复制 public void click(View view) {
Snackbar.make(view, "这是一个提示", Snackbar.LENGTH_SHORT).show();
}
其中第二个参数是提示内容,第三个参数是消失的时间
Snackbar.gif
我们需要给与用户点击反馈后才能消除提示的情况下,可以这样使用
代码语言:javascript复制 public void click(View view) {
Snackbar.make(view, "这是一个提示", Snackbar.LENGTH_INDEFINITE).setAction("确定", new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "点击了确定", Toast.LENGTH_SHORT).show();
}
}).show();
}
把消失时间传入Snackbar.LENGTH_INDEFINITE,点击完成后Snackbar会自动消失
Snackbar.gif
我们还可以利用setCallback方法监听SnackBar的显示和消失,利用setActionTextColor方法设置点击按钮的字体颜色
代码语言:javascript复制 public void click(View view) {
Snackbar.make(view, "这是一个提示", Snackbar.LENGTH_INDEFINITE).setAction("确定", new View.OnClickListener() {
@Override
public void onClick(View v) {
}
}).setCallback(new Snackbar.Callback() {
@Override
public void onShown(Snackbar sb) {
Toast.makeText(MainActivity.this, "显示了", Toast.LENGTH_SHORT).show();
super.onShown(sb);
}
@Override
public void onDismissed(Snackbar transientBottomBar, int event) {
Toast.makeText(MainActivity.this, "消失了", Toast.LENGTH_SHORT).show();
super.onDismissed(transientBottomBar, event);
}
}).setActionTextColor(Color.YELLOW).show();
}
Snackbar.gif
通常情况下,我们无法修改提示的颜色和字体大小,如果我们想要修改的话,就从分析Snackbar的源码开始,了解这个控件的内部
首先我们来看Snackbar的make方法
代码语言:javascript复制 /**
* Make a Snackbar to display a message
*
* <p>Snackbar will try and find a parent view to hold Snackbar's view from the value given
* to {@code view}. Snackbar will walk up the view tree trying to find a suitable parent,
* which is defined as a {@link CoordinatorLayout} or the window decor's content view,
* whichever comes first.
*
* <p>Having a {@link CoordinatorLayout} in your view hierarchy allows Snackbar to enable
* certain features, such as swipe-to-dismiss and automatically moving of widgets like
* {@link FloatingActionButton}.
*
* @param view The view to find a parent from.
* @param text The text to show. Can be formatted text.
* @param duration How long to display the message. Either {@link #LENGTH_SHORT} or {@link
* #LENGTH_LONG}
*/
@NonNull
public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
@Duration int duration) {
final ViewGroup parent = findSuitableParent(view);
if (parent == null) {
throw new IllegalArgumentException("No suitable parent found from the given view. "
"Please provide a valid view.");
}
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final SnackbarContentLayout content =
(SnackbarContentLayout) inflater.inflate(
R.layout.design_layout_snackbar_include, parent, false);
final Snackbar snackbar = new Snackbar(parent, content, content);
snackbar.setText(text);
snackbar.setDuration(duration);
return snackbar;
}
首先它调用了findSuitableParent方法,传入我们在make方法中传入的view,获取了一个ViewGroup
代码语言:javascript复制 private static ViewGroup findSuitableParent(View view) {
ViewGroup fallback = null;
do {
if (view instanceof CoordinatorLayout) {
// We've found a CoordinatorLayout, use it
return (ViewGroup) view;
} else if (view instanceof 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);
// If we reach here then we didn't find a CoL or a suitable content view so we'll fallback
return fallback;
}
findSuitableParent方法很有意思,它会不断的寻找父容器
如果是CoordinatorLayout的话,就直接返回,所以说我们使用CoordinatorLayout,是可以改变Snackbar的显示位置的;
如果是id为content的FrameLayout的话就直接返回这个FrameLayout,之前分析Activity启动源码时,我们了解到我们自己写的布局,最终会被添加到DecorView的一个id为content的FrameLayout上,所以这就是一般情况下Snackbar是显示在最下方的原因
回到make方法
代码语言:javascript复制 public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
@Duration int duration) {
final ViewGroup parent = findSuitableParent(view);
if (parent == null) {
throw new IllegalArgumentException("No suitable parent found from the given view. "
"Please provide a valid view.");
}
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final SnackbarContentLayout content =
(SnackbarContentLayout) inflater.inflate(
R.layout.design_layout_snackbar_include, parent, false);
final Snackbar snackbar = new Snackbar(parent, content, content);
snackbar.setText(text);
snackbar.setDuration(duration);
return snackbar;
}
之后,又加载了一个design_layout_snackbar_include的布局,我们看下这个布局
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<view
xmlns:android="http://schemas.android.com/apk/res/android"
class="android.support.design.internal.SnackbarContentLayout"
android:theme="@style/ThemeOverlay.AppCompat.Dark"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom">
<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"
android:textAlignment="viewStart"/>
<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:minWidth="48dp"
android:visibility="gone"
android:textColor="?attr/colorAccent"
style="?attr/borderlessButtonStyle"/>
</view>
很明显,这就是我们的提示信息和点击按钮,我们继续看make方法,之后它实例化了Snackbar :final Snackbar snackbar = new Snackbar(parent, content, content);其中parent为id为content的FrameLayout(暂时不考虑CoordinatorLayout),content为上面的布局
来看Snackbar的构造方法,发现最终调用的父类BaseTransientBottomBar的构造方法
代码语言:javascript复制 protected BaseTransientBottomBar(@NonNull ViewGroup parent, @NonNull View content,
@NonNull ContentViewCallback contentViewCallback) {
if (parent == null) {
throw new IllegalArgumentException("Transient bottom bar must have non-null parent");
}
if (content == null) {
throw new IllegalArgumentException("Transient bottom bar must have non-null content");
}
if (contentViewCallback == null) {
throw new IllegalArgumentException("Transient bottom bar must have non-null callback");
}
mTargetParent = parent;
mContentViewCallback = contentViewCallback;
mContext = parent.getContext();
ThemeUtils.checkAppCompatTheme(mContext);
LayoutInflater inflater = LayoutInflater.from(mContext);
// Note that for backwards compatibility reasons we inflate a layout that is defined
// in the extending Snackbar class. This is to prevent breakage of apps that have custom
// coordinator layout behaviors that depend on that layout.
mView = (SnackbarBaseLayout) inflater.inflate(
R.layout.design_layout_snackbar, mTargetParent, false);
mView.addView(content);
ViewCompat.setAccessibilityLiveRegion(mView,
ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE);
ViewCompat.setImportantForAccessibility(mView,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
// Make sure that we fit system windows and have a listener to apply any insets
ViewCompat.setFitsSystemWindows(mView, true);
ViewCompat.setOnApplyWindowInsetsListener(mView,
new android.support.v4.view.OnApplyWindowInsetsListener() {
@Override
public WindowInsetsCompat onApplyWindowInsets(View v,
WindowInsetsCompat insets) {
// Copy over the bottom inset as padding so that we're displayed
// above the navigation bar
v.setPadding(v.getPaddingLeft(), v.getPaddingTop(),
v.getPaddingRight(), insets.getSystemWindowInsetBottom());
return insets;
}
});
mAccessibilityManager = (AccessibilityManager)
mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
}
这边先将parent赋值给mTargetParent,又加载了一个布局赋值给mView ,并添加了content(design_layout_snackbar_include.xml生成的View),我们看下mView的布局文件design_layout_snackbar
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<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"
android:theme="@style/ThemeOverlay.AppCompat.Dark"
style="@style/Widget.Design.Snackbar" />
这边只是将内容(content)再包装一层容器(mView),可以更好的管理样式
在BaseTransientBottomBar类中,我们还发现了值得注意的成员变量,这边先记一下
代码语言:javascript复制 static {
sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_SHOW:
((BaseTransientBottomBar) message.obj).showView();
return true;
case MSG_DISMISS:
((BaseTransientBottomBar) message.obj).hideView(message.arg1);
return true;
}
return false;
}
});
}
final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
@Override
public void show() {
sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, BaseTransientBottomBar.this));
}
@Override
public void dismiss(int event) {
sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0,
BaseTransientBottomBar.this));
}
};
好了,Snackbar的初始化,到这边就结束了,我们继续来看Snackbar的show()方法
代码语言:javascript复制 public void show() {
SnackbarManager.getInstance().show(mDuration, mManagerCallback);
}
public void show(int duration, Callback callback) {
synchronized (mLock) {
if (isCurrentSnackbarLocked(callback)) {
// Means that the callback is already in the queue. We'll just update the duration
mCurrentSnackbar.duration = duration;
// If this is the Snackbar currently being shown, call re-schedule it's
// timeout
mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
scheduleTimeoutLocked(mCurrentSnackbar);
return;
} else if (isNextSnackbarLocked(callback)) {
// 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)) {
// 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
showNextSnackbarLocked();
}
}
}
其中的callback就是我们上面记录的mManagerCallback,再看mNextSnackbar = new SnackbarRecord(duration, callback);这段代码
代码语言:javascript复制 private static class SnackbarRecord {
final WeakReference<Callback> callback;
int duration;
boolean paused;
SnackbarRecord(int duration, Callback callback) {
this.callback = new WeakReference<>(callback);
this.duration = duration;
}
boolean isSnackbar(Callback callback) {
return callback != null && this.callback.get() == callback;
}
}
这边将mManagerCallback使用了弱引用,我们继续往下,来到showNextSnackbarLocked();方法
代码语言:javascript复制 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;
}
}
}
如果没有被回收,则调用mManagerCallback的show方法,我们再看下mManagerCallback
代码语言:javascript复制 final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
@Override
public void show() {
sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, BaseTransientBottomBar.this));
}
@Override
public void dismiss(int event) {
sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0,
BaseTransientBottomBar.this));
}
};
由于这边使用到了Handler,所以防止内存泄漏,使用了弱引用,再来看sHandler对象
代码语言:javascript复制 static {
sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_SHOW:
((BaseTransientBottomBar) message.obj).showView();
return true;
case MSG_DISMISS:
((BaseTransientBottomBar) message.obj).hideView(message.arg1);
return true;
}
return false;
}
});
}
调用了showView方法
代码语言:javascript复制 final void showView() {
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;
final Behavior behavior = new Behavior();
behavior.setStartAlphaSwipeDistance(0.1f);
behavior.setEndAlphaSwipeDistance(0.6f);
behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
@Override
public void onDismiss(View view) {
view.setVisibility(View.GONE);
dispatchDismiss(BaseCallback.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, pause the timeout
SnackbarManager.getInstance().pauseTimeout(mManagerCallback);
break;
case SwipeDismissBehavior.STATE_IDLE:
// If the view has been released and is idle, restore the timeout
SnackbarManager.getInstance()
.restoreTimeoutIfPaused(mManagerCallback);
break;
}
}
});
clp.setBehavior(behavior);
// Also set the inset edge so that views can dodge the bar correctly
clp.insetEdge = Gravity.BOTTOM;
}
mTargetParent.addView(mView);
}
mView.setOnAttachStateChangeListener(
new BaseTransientBottomBar.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {}
@Override
public void onViewDetachedFromWindow(View v) {
if (isShownOrQueued()) {
// If we haven't already been dismissed then this event is coming from a
// non-user initiated action. Hence we need to make sure that we callback
// and keep our state up to date. We need to post the call since
// removeView() will call through to onDetachedFromWindow and thus overflow.
sHandler.post(new Runnable() {
@Override
public void run() {
onViewHidden(BaseCallback.DISMISS_EVENT_MANUAL);
}
});
}
}
});
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 BaseTransientBottomBar.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();
}
}
});
}
}
里面有一行关键代码mTargetParent.addView(mView);
mTargetParent是DecorView中的id为content的FrameLayout,mView就是之前解析xml的Snackbar,最终通过addView方法将Snackbar显示出来。
SnackBar时序图.png