SnackBar 源码bug修复、源码解析
来源:互联网 发布:马赛克拼图软件 编辑:程序博客网 时间:2024/04/30 22:51
前言:为什么写这篇博客?原因是:在使用和封装Snackbar 后测试出来一个bug ,在反复显示多个Snackbar后 再也不显示了。 这就让人很抓狂啊!!!于是通读了Snackbar源码,修正了这个bug。 我使用的是TsnackBar,从github 上down的源码,https://github.com/guoyoujin/MySnackBar;这个项目的作者只对Snackbar添加了顶部及底部显示和三种显示主题,在文末附上我修改的TSnackbar,添加了上滑/下滑隐藏Snackbar,并修复了我所提到的bug。
SnackBar 是 android 中一种 用来做消息提示的控件,它不继承自View, 而是内部 管理 SnackbarLayout 隐藏与显示; SnackbarLayout 是SnackBar的内部类, 继承于LinearLayout;
你可以在 android.support:design 包下找到SnackBar;
使用方式就不多说了,进入正题;
1. 源码bug修复:
show了多个Snackbar后不知道为什么show不出来怎么办?只需要做如下修改:
//Snackbar 内函数: final void hideView(@Callback.DismissEvent final int event) { //将原先的(view.getVisibility() == mView.VISIBLE) 改为mView.isShown() 即可; //getVisibility 返回为 Visible只是说明当前view 的显示状态,isShown 返回true说明这个view和所有的父view 都可见; 在这里只有当都可见咱们才执行animateViewOut(),亲测解决了此问题; if (shouldAnimate() && mView.isShown()) { animateViewOut(event); } else { // If anims are disabled or the view isn't visible, just call back now onViewHidden(event); } }
2. 源码解析:
有三个核心类:SnackBar 、SnackbarManager、SnackbarRecord ;
那它们是如何工作的呢?
SnackBar 管理SnackbarLayout 隐藏与显示;
SnackbarManager 通知 SnackBar 中SnackbarLayout 隐藏与显示;
SnackbarRecord 中保存了Snackbar 的Callback 和SnackBar的显示时长,SnackbarManager 定义了两个SnackbarRecord :mCurrentSnackbar 和 mNextSnackbar;
1.先从第一次调用 snackbar 的show 说起:
/** *SnackBar 中的show * Show the {@link Snackbar}. */ public void show() { //调用 SnackbarManager 中的show SnackbarManager.getInstance().show(mDuration, mManagerCallback); }
//SnackbarManager 中的show (请理解注释) 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(同样的,下一个想要显示snackbar的回调早已在队列中。我们只需要更新时长) mNextSnackbar.duration = duration; } else { //③ // Else, we need to create a new record and queue it(代表这是下一个需要show的 snackbar ) 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(如果当前有显示的Snackbar,尝试去取消显示) return; } else { //⑤ // Clear out the current snackbar mCurrentSnackbar = null; // Otherwise, just show it now(如果当前没有要显示的就显示下一个Snackbar) showNextSnackbarLocked(); } } }
调用snackbar 的show 后,会继续调用 snackbarManager的 show ,然后在 snackbarManager的 show 中按顺序执行③⑤代码块;
⑤中执行的showNextSnackbarLocked函数如下:
// SnackbarManager 中的函数 private void showNextSnackbarLocked() { if (mNextSnackbar != null) { //把下一个想要显示的赋值给mCurrentSnackbar ; mCurrentSnackbar = mNextSnackbar; mNextSnackbar = null; final Callback callback = mCurrentSnackbar.callback.get(); if (callback != null) { //Callback 中有show 和 dismiss callback.show();//实际调用的是如下的mManagerCallback 的show } else { // The callback doesn't exist any more, clear out the Snackbar mCurrentSnackbar = null; } } }
//showNextSnackbarLocked() 中的变量final Callback callback 是 mManagerCallback,它是定义在snackbar 中的成员变量, 早在 SnackbarManager 中的show (int duration, Callback callback) 传入; private final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() { @Override public void show() { //发送一条消息让这个Snackbar 显示 sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this)); } @Override public void dismiss(int event) { //发送一条消息让这个Snackbar取消显示 sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, Snackbar.this)); } };
调用mManagerCallback 的show后 会发一条消息,然后由主线程中的Looper 循环处理消息:
//Snackbar 中的代码块 static { sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { @Override public boolean handleMessage(Message message) { switch (message.what) { case MSG_SHOW: ((Snackbar) message.obj).showView();//至此第一个snackbar 就完全显示出来啦! return true; case MSG_DISMISS: ((Snackbar) message.obj).hideView(message.arg1); return true; } return false; } }); }
// Snackbar 中的函数,最终显示一个Snackbar ,显示有两种方式:1.直接显示,2.通过播放动画显示;具体是通过shouldAnimate()控制的,内部只有一句代码:“mAccessibilityManager.isEnabled()” , AccessibilityManager 是一种系统服务(无障碍服务,http://www.xuebuyuan.com/2061597.html),大家在逢年过节可能用过微信红包助手之类的软件,就是利用这个服务实现的,有兴趣的大家可以阅读这篇文章:http://blog.csdn.net/h183288132/article/details/50973112 final void showView() { //......具体是如何添加到窗口上的 ,由于篇幅原因我就不叙述了(so easy) if (ViewCompat.isLaidOut(mView)) { //如果SnackbarLayout 已经摆放好了执行显示动画; animateViewIn(); } else { mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() { @Override public void onLayoutChange(View view, int left, int top, int right, int bottom) { animateViewIn(); mView.setOnLayoutChangeListener(null); } }); } mView.setOnAttachStateChangeListener(new SnackbarLayout.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. //如果view在规定时间内与window 解绑(这时还在排队中), 就执行这个以保证 更新状态,取消显示此Snackbar; sHandler.post(new Runnable() { @Override public void run() { onViewHidden(Callback.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 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(); } } }); } }
那显示出来之后是如何在 之前规定 的一段时间后取消显示呢? 相信大家都已经猜到了, 哈哈就是如下代码:
在animateViewIn()中设置了动画监听器:
anim.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationEnd(Animation animation) { onViewShown();//在显示动画执行完后调用onViewShown; } @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } });
//通知SnackbarManager Snackbar 已经显示了。 private void onViewShown() { SnackbarManager.getInstance().onShown(mManagerCallback); if (mCallback != null) { mCallback.onShown(this); } }
// SnackbarManager 中的函数 public void onShown(Callback callback) { synchronized (mLock) { if (isCurrentSnackbarLocked(callback)) { //这里发送消息 在规定显示时长后取消显示Snackbar; scheduleTimeoutLocked(mCurrentSnackbar); } } }
// SnackbarManager 中的函数 用来在规定显示时长后取消Snackbar private void scheduleTimeoutLocked(SnackbarRecord r) { if (r.duration == Snackbar.LENGTH_INDEFINITE) { // If we're set to indefinite, we don't want to set a timeout return; } int durationMs = LONG_DURATION_MS; if (r.duration > 0) { durationMs = r.duration; } else if (r.duration == Snackbar.LENGTH_SHORT) { durationMs = SHORT_DURATION_MS; } mHandler.removeCallbacksAndMessages(r); //发送出去的消息被处理后 第一次Snackbar显示与消失就完成啦! mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_TIMEOUT, r), durationMs); }
2.如果先后连续显示两个Snackbar(记为A,B)又是如何控制的呢
第一次show 如同以上分析,第二次显示就不同了,第二次会依次调用SnackbarManager 中的show ③④,看到的现象是A在规定时长内取消显示接着显示B;
原因是:
//SnackbarManager的show() 中的④: private boolean cancelSnackbarLocked(SnackbarRecord record, int event) { Callback callback = record.callback.get(); if (callback != null) { // Make sure we remove any timeouts for the SnackbarRecord mHandler.removeCallbacksAndMessages(record); callback.dismiss(event);//至此A就隐藏了。 return true; } return false; }在调用callback.dismiss(event) 后会调用: public void onDismissed(Callback callback) { synchronized (mLock) { if (isCurrentSnackbarLocked(callback)) { // If the callback is from a Snackbar currently show, remove it and show a new one mCurrentSnackbar = null; if (mNextSnackbar != null) { //显示下一个Snackbar:至此B就显示出来了 showNextSnackbarLocked(); } } } }
我update 的 Tsnackbar:
/*
* 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.
*/
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.ColorInt;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.SwipeDismissBehavior;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.LinearInterpolator;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* TSnackbar provides lightweight feedback about an operation. They show a brief message at the
* top of the screen on mobile. TSnackbar appear above all other
* elements on screen and only one can be displayed at a time.
*
* They automatically disappear after a timeout or after user interaction elsewhere on the screen,
* particularly after interactions that summon a new surface or activity. Snackbars can be swiped
* off screen.
*
* Snackbars can contain an action which is set via
* {@link #setAction(CharSequence, View.OnClickListener)}.
*
* To be notified when a snackbar has been shown or dismissed, you can provide a {@link Callback}
* via {@link #setCallback(Callback)}.
*/
public final class TSnackbar {
/** * Callback class for {@link TSnackbar} instances. * * @see TSnackbar#setCallback(Callback) */public static abstract class Callback { /** * Indicates that the TSnackbar was dismissed via a swipe. */ public static final int DISMISS_EVENT_SWIPE = 0; /** * Indicates that the TSnackbar was dismissed via an action click. */ public static final int DISMISS_EVENT_ACTION = 1; /** * Indicates that the TSnackbar was dismissed via a timeout. */ public static final int DISMISS_EVENT_TIMEOUT = 2; /** * Indicates that the TSnackbar was dismissed via a call to {@link #dismiss()}. */ public static final int DISMISS_EVENT_MANUAL = 3; /** * Indicates that the TSnackbar was dismissed from a new Snackbar being shown. */ public static final int DISMISS_EVENT_CONSECUTIVE = 4; @IntDef({DISMISS_EVENT_SWIPE, DISMISS_EVENT_ACTION, DISMISS_EVENT_TIMEOUT, DISMISS_EVENT_MANUAL, DISMISS_EVENT_CONSECUTIVE}) @Retention(RetentionPolicy.SOURCE) public @interface DismissEvent { } /** * Called when the given {@link TSnackbar} has been dismissed, either through a time-out, * having been manually dismissed, or an action being clicked. * * @param TSnackbar The snackbar which has been dismissed. * @param event The event which caused the dismissal. One of either: * {@link #DISMISS_EVENT_SWIPE}, {@link #DISMISS_EVENT_ACTION}, * {@link #DISMISS_EVENT_TIMEOUT}, {@link #DISMISS_EVENT_MANUAL} or * {@link #DISMISS_EVENT_CONSECUTIVE}. * @see TSnackbar#dismiss() */ public void onDismissed(TSnackbar TSnackbar, @DismissEvent int event) { } /** * Called when the given {@link TSnackbar} is visible. * * @param TSnackbar The snackbar which is now visible. * @see TSnackbar#show() */ public void onShown(TSnackbar TSnackbar) { }}@IntDef({APPEAR_TOP, APPEAR_BOTTOM})@Retention(RetentionPolicy.SOURCE)public @interface OverSnackAppearDirection {}/** * Show the TSnackbar from top to down. */public static final int APPEAR_TOP = 0;/** * Show the TSnackbar from top to down. */public static final int APPEAR_BOTTOM = 1;@IntDef({LENGTH_INDEFINITE, LENGTH_SHORT, LENGTH_LONG})@Retention(RetentionPolicy.SOURCE)public @interface Duration {}/** * Show the TSnackbar indefinitely. This means that the TSnackbar will be displayed from the time * that is {@link #show() shown} until either it is dismissed, or another TSnackbar is shown. * * @see #setDuration */public static final int LENGTH_INDEFINITE = -2;/** * Show the TSnackbar for a short period of time. * * @see #setDuration */public static final int LENGTH_SHORT = -1;/** * Show the TSnackbar for a long period of time. * * @see #setDuration */public static final int LENGTH_LONG = 0;private static final int ANIMATION_DURATION = 250;private static final int ANIMATION_FADE_DURATION = 180;private static final Handler sHandler;private static final int MSG_SHOW = 0;private static final int MSG_DISMISS = 1;@OverSnackAppearDirectionprivate int appearDirection = APPEAR_TOP;static { sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { @Override public boolean handleMessage(Message message) { switch (message.what) { case MSG_SHOW: ((TSnackbar) message.obj).showView(); return true; case MSG_DISMISS: ((TSnackbar) message.obj).hideView(message.arg1); return true; } return false; } });}private final ViewGroup mParent;private final Context mContext;private final SnackbarLayout mView;private int mDuration;private Callback mCallback;private final AccessibilityManager mAccessibilityManager;private TSnackbar(ViewGroup parent) { appearDirection = APPEAR_TOP; mParent = parent; mContext = parent.getContext(); LayoutInflater inflater = LayoutInflater.from(mContext); if (appearDirection == APPEAR_BOTTOM) { mView = (SnackbarLayout) inflater.inflate(R.layout.view_bsnackbar_layout, mParent, false); } else { mView = (SnackbarLayout) inflater.inflate(R.layout.view_tsnackbar_layout, mParent, false); } mAccessibilityManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); setSwipeListener();}private TSnackbar(ViewGroup parent, final @OverSnackAppearDirection int appearDirection) { this.appearDirection = appearDirection; mParent = parent; mContext = parent.getContext(); LayoutInflater inflater = LayoutInflater.from(mContext); if (appearDirection == APPEAR_BOTTOM) { mView = (SnackbarLayout) inflater.inflate(R.layout.view_bsnackbar_layout, mParent, false); } else { mView = (SnackbarLayout) inflater.inflate(R.layout.view_tsnackbar_layout, mParent, false); } mAccessibilityManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); if (appearDirection == APPEAR_TOP) { setMinHeight(0, 0); } setSwipeListener();}private void setSwipeListener() { mView.setOnTouchListener(new View.OnTouchListener() { private float yStart; private float yEnd; @Override public boolean onTouch(View v, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { yStart = event.getRawY(); Logger.d("onTouch:ACTION_DOWN", "yStart:" + yStart); } else if (event.getAction() == MotionEvent.ACTION_MOVE) { yEnd = event.getRawY(); Logger.d("onTouch:ACTION_MOVE", "yStart:" + yStart + "," + "yEnd:" + yEnd); if (appearDirection == APPEAR_TOP) { if (yStart - yEnd > mView.getMinimumHeight() / 2) { Logger.d("onTouch:DISMISS_EVENT_SWIPE", "yStart:" + yStart + "," + "yEnd:" + yEnd); hideView(Callback.DISMISS_EVENT_SWIPE); yStart = 0; yEnd = 0; return true; } } else { if (yEnd - yStart > mView.getMinimumHeight() / 2) { Logger.d("onTouch:DISMISS_EVENT_SWIPE", "yStart:" + yStart + "," + "yEnd:" + yEnd); hideView(Callback.DISMISS_EVENT_SWIPE); yStart = 0; yEnd = 0; return true; } } } else { Logger.d("onTouch:ACTION_OTHER", "yStart:" + yStart + "," + "yEnd:" + yEnd); yStart = 0; yEnd = 0; } return false; } });}/** * @param stateBarHeight * @param actionBarHeight * @return */public TSnackbar setMinHeight(int stateBarHeight, int actionBarHeight) { if (appearDirection == APPEAR_TOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (stateBarHeight > 0 || actionBarHeight > 0) { mView.setPadding(0, stateBarHeight, 0, 0); mView.setMinimumHeight(stateBarHeight + actionBarHeight); } else { mView.setPadding(0, ScreenUtil.getStatusHeight(mContext), 0, 0); mView.setMinimumHeight((int) mContext.getResources().getDimension(R.dimen.toolbar_height) + ScreenUtil.getStatusHeight(mContext)); } } else { if (stateBarHeight > 0 || actionBarHeight > 0) { mView.setMinimumHeight(actionBarHeight); ScreenUtil.setMargins(mView, 0, stateBarHeight, 0, 0); } else { mView.setMinimumHeight((int) mContext.getResources().getDimension(R.dimen.toolbar_height)); ScreenUtil.setMargins(mView, 0, ScreenUtil.getStatusHeight(mContext), 0, 0); } } } return this;}/** * Make a TSnackbar to display a message * <p/> * <p>TSnackbar will try and find a parent view to hold TSnackbar'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/> * <p>Having a {@link CoordinatorLayout} in your view hierarchy allows TSnackbar to enable * certain features, such as swipe-to-dismiss and automatically moving of widgets like * * @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} */@NonNullpublic static TSnackbar make(@NonNull View view, @NonNull CharSequence text, @Duration int duration) { TSnackbar tSnackbar = new TSnackbar(findSuitableParent(view), APPEAR_TOP); tSnackbar.setText(text); tSnackbar.setDuration(duration); return tSnackbar;}@NonNullpublic static TSnackbar make(@NonNull View view, @NonNull CharSequence text, @Duration int duration, @OverSnackAppearDirection int appearDirection) { TSnackbar tSnackbar = new TSnackbar(findSuitableParent(view), appearDirection); tSnackbar.setText(text); tSnackbar.setDuration(duration); return tSnackbar;}/** * Make a TSnackbar to display a message. * <p/> * <p>TSnackbar will try and find a parent view to hold TSnackbar's view from the value given * to {@code view}. TSnackbar 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/> * <p>Having a {@link CoordinatorLayout} in your view hierarchy allows TSnackbar to enable * certain features, such as swipe-to-dismiss and automatically moving of widgets like * * @param view The view to find a parent from. * @param resId The resource id of the string resource to use. Can be formatted text. * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or {@link * #LENGTH_LONG} */@NonNullpublic static TSnackbar make(@NonNull View view, @StringRes int resId, @Duration int duration) { return make(view, view.getResources().getText(resId), duration);}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; //return (ViewGroup) view;}/** * @param resource_id * @return */public TSnackbar addIcon(int resource_id) { final TextView tv = mView.getMessageView(); tv.setCompoundDrawablesWithIntrinsicBounds(mContext.getResources().getDrawable(resource_id), null, null, null); return this;}/** * @param resource_id image id * @param width image width * @param height image height * @return */public TSnackbar addIcon(int resource_id, int width, int height) { final TextView tv = mView.getMessageView(); if (width > 0 || height > 0) { tv.setCompoundDrawablesWithIntrinsicBounds(new BitmapDrawable(Bitmap.createScaledBitmap(((BitmapDrawable) (mContext.getResources().getDrawable(resource_id))).getBitmap(), width, height, true)), null, null, null); } else { addIcon(resource_id); } return this;}/** * show loading progressBar * * @param resource_id image id * @param left show textview left * @param right show textview right * @return TSnackbar */public TSnackbar addIconProgressLoading(int resource_id, boolean left, boolean right) { Drawable drawable = mContext.getResources().getDrawable(R.drawable.rotate); if (resource_id > 0) { drawable = mContext.getResources().getDrawable(resource_id); } addIconProgressLoading(drawable, left, right); return this;}/** * @param drawable * @param left * @param right * @return */public TSnackbar addIconProgressLoading(Drawable drawable, boolean left, boolean right) { final ObjectAnimator animator = ObjectAnimator.ofInt(drawable, "level", 0, 10000); animator.setDuration(1000); animator.setInterpolator(new LinearInterpolator()); animator.setRepeatCount(ValueAnimator.INFINITE); animator.setRepeatMode(ValueAnimator.REVERSE); mView.setBackgroundColor(mContext.getResources().getColor(Prompt.SUCCESS.getBackgroundColor())); if (left) { mView.getMessageView().setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); } if (right) { mView.getMessageView().setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null); } animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { if (mCallback != null) { mCallback.onShown(TSnackbar.this); } SnackbarManager.getInstance().onShown(mManagerCallback); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); animator.start(); return this;}/** * default style {ERROR , WARNING , SUCCESS} * * @param prompt * @return */public TSnackbar setPromptThemeBackground(Prompt prompt) { if (prompt == Prompt.SUCCESS) { setBackgroundColor(mContext.getResources().getColor(Prompt.SUCCESS.getBackgroundColor())); addIcon(Prompt.SUCCESS.getResIcon(), 0, 0); } else if (prompt == Prompt.ERROR) { setBackgroundColor(mContext.getResources().getColor(Prompt.ERROR.getBackgroundColor())); addIcon(Prompt.ERROR.getResIcon(), 0, 0); } else if (prompt == Prompt.WARNING) { setBackgroundColor(mContext.getResources().getColor(Prompt.WARNING.getBackgroundColor())); addIcon(Prompt.WARNING.getResIcon(), 0, 0); } return this;}/** * @param colorId * @return */public TSnackbar setBackgroundColor(int colorId) { mView.setBackgroundColor(colorId); return this;}/** * Set the action to be displayed in this {@link TSnackbar}. * * @param resId String resource to display * @param listener callback to be invoked when the action is clicked */@NonNullpublic TSnackbar setAction(@StringRes int resId, View.OnClickListener listener) { return setAction(mContext.getText(resId), listener);}/** * Set the action to be displayed in this {@link TSnackbar}. * * @param text Text to display * @param listener callback to be invoked when the action is clicked */@NonNullpublic TSnackbar setAction(CharSequence text, final View.OnClickListener listener) { final TextView tv = mView.getActionView(); if (TextUtils.isEmpty(text) || listener == null) { tv.setVisibility(View.GONE); tv.setOnClickListener(null); } else { tv.setVisibility(View.VISIBLE); tv.setText(text); tv.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { listener.onClick(view); dispatchDismiss(Callback.DISMISS_EVENT_ACTION); } }); } return this;}/** * Sets the text color of the action specified in * {@link #setAction(CharSequence, View.OnClickListener)}. */@NonNullpublic TSnackbar setActionTextColor(ColorStateList colors) { final Button btn = mView.getActionView(); btn.setTextColor(colors); return this;}/** * Sets the text color of the action specified in */@NonNullpublic TSnackbar setActionTextSize(int size) { final Button btn = mView.getActionView(); btn.setTextSize(size); return this;}/** * Sets the text color of the action specified in */@NonNullpublic TSnackbar setMessageTextSize(int size) { final TextView tv = mView.getMessageView(); tv.setTextSize(size); return this;}/** * Sets the text color of the action specified in */@NonNullpublic TSnackbar setActionTextColor(@ColorInt int color) { final Button btn = mView.getActionView(); btn.setTextColor(color); return this;}/** * Sets the text color of the action specified in */@NonNullpublic TSnackbar setTextColor(@ColorInt int color) { setActionTextColor(color); setMessageTextColor(color); return this;}/** * Sets the text color of the action specified in * {@link #setAction(CharSequence, View.OnClickListener)}. */@NonNullpublic TSnackbar setColor(ColorStateList colors) { setActionTextColor(colors); setMessageTextColor(colors); return this;}/** * Sets the text color of the action specified in * {@link #setAction(CharSequence, View.OnClickListener)}. */@NonNullpublic TSnackbar setMessageTextColor(@ColorInt int color) { final TextView tv = mView.getMessageView(); tv.setTextColor(color); return this;}/** * Sets the text color of the action specified in * {@link #setAction(CharSequence, View.OnClickListener)}. */@NonNullpublic TSnackbar setMessageTextColor(ColorStateList colors) { final TextView tv = mView.getMessageView(); tv.setTextColor(colors); return this;}/** * Update the text in this {@link TSnackbar}. * * @param message The new text for the Toast. */@NonNullpublic TSnackbar setText(@NonNull CharSequence message) { final TextView tv = mView.getMessageView(); tv.setText(message); return this;}/** * Update the text in this {@link TSnackbar}. * * @param resId The new text for the Toast. */@NonNullpublic TSnackbar setText(@StringRes int resId) { return setText(mContext.getText(resId));}/** * Set how long to show the view for. * * @param duration either be one of the predefined lengths: * {@link #LENGTH_SHORT}, {@link #LENGTH_LONG}, or a custom duration * in milliseconds. */@NonNullpublic TSnackbar setDuration(@Duration int duration) { mDuration = duration; return this;}/** * @param direction {@link #APPEAR_BOTTOM}, {@link #APPEAR_TOP} * @return */@NonNullpublic TSnackbar setAppearDirection(int direction) { appearDirection = direction; return this;}/** * Return the duration. * * @see #setDuration */@Durationpublic int getDuration() { return mDuration;}/** * Returns the {@link TSnackbar}'s view. */@NonNullpublic View getView() { return mView;}/** * Show the {@link TSnackbar}. */public void show() { SnackbarManager.getInstance().show(mDuration, mManagerCallback);}/** * Dismiss the {@link TSnackbar}. */public void dismiss() { dispatchDismiss(Callback.DISMISS_EVENT_MANUAL);}private void dispatchDismiss(@Callback.DismissEvent int event) { SnackbarManager.getInstance().dismiss(mManagerCallback, event);}/** * Set a callback to be called when this the visibility of this {@link TSnackbar} changes. */@NonNullpublic TSnackbar setCallback(Callback callback) { mCallback = callback; return this;}/** * Return whether this {@link TSnackbar} is currently being shown. */public boolean isShown() { return SnackbarManager.getInstance().isCurrent(mManagerCallback);}/** * Returns whether this {@link TSnackbar} is currently being shown, or is queued to be * shown next. */public boolean isShownOrQueued() { return SnackbarManager.getInstance().isCurrentOrNext(mManagerCallback);}private final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() { @Override public void show() { sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, TSnackbar.this)); } @Override public void dismiss(int event) { sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, TSnackbar.this)); }};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 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(Callback.DISMISS_EVENT_SWIPE); } @Override public void onDragStateChanged(int state) { switch (state) { case SwipeDismissBehavior.STATE_DRAGGING: case SwipeDismissBehavior.STATE_SETTLING: SnackbarManager.getInstance().pauseTimeout(mManagerCallback); break; case SwipeDismissBehavior.STATE_IDLE: SnackbarManager.getInstance().restoreTimeoutIfPaused(mManagerCallback); break; } } }); ((CoordinatorLayout.LayoutParams) lp).setBehavior(behavior); ((CoordinatorLayout.LayoutParams) lp).setMargins(0, -30, 0, 0); } mParent.addView(mView); } if (ViewCompat.isLaidOut(mView)) { animateViewIn(); } else { mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() { @Override public void onLayoutChange(View view, int left, int top, int right, int bottom) { animateViewIn(); mView.setOnLayoutChangeListener(null); } }); } mView.setOnAttachStateChangeListener(new SnackbarLayout.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(Callback.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 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(); } } }); }}private void animateViewIn() { Animation anim; if (appearDirection == APPEAR_TOP) { anim = getAnimationInFromTopToDown(); } else { anim = getAnimationInFromBottomToTop(); } anim.setInterpolator(new FastOutSlowInInterpolator()); anim.setDuration(ANIMATION_DURATION); anim.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationEnd(Animation animation) { onViewShown(); } @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } }); mView.startAnimation(anim);}private void animateViewOut(final int event) { Animation anim; if (appearDirection == APPEAR_TOP) { anim = getAnimationOutFromTopToDown(); } else { anim = getAnimationOutFromBottomToTop(); } anim.setInterpolator(new FastOutSlowInInterpolator()); anim.setDuration(ANIMATION_DURATION); anim.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationEnd(Animation animation) { onViewHidden(event); } @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } }); mView.startAnimation(anim);}private Animation getAnimationInFromTopToDown() { return AnimationUtils.loadAnimation(mView.getContext(), R.anim.top_in);}private Animation getAnimationInFromBottomToTop() { return AnimationUtils.loadAnimation(mView.getContext(), R.anim.design_snackbar_in);}private Animation getAnimationOutFromTopToDown() { return AnimationUtils.loadAnimation(mView.getContext(), R.anim.top_out);}private Animation getAnimationOutFromBottomToTop() { return AnimationUtils.loadAnimation(mView.getContext(), R.anim.design_snackbar_out);}final void hideView(@Callback.DismissEvent final int event) { if (shouldAnimate() && mView.isShown()) { animateViewOut(event); } else { // If anims are disabled or the view isn't visible, just call back now onViewHidden(event); }}private void onViewShown() { SnackbarManager.getInstance().onShown(mManagerCallback); if (mCallback != null) { mCallback.onShown(this); }}private void onViewHidden(int event) { // First tell the SnackbarManager that it has been dismissed SnackbarManager.getInstance().onDismissed(mManagerCallback); // Now call the dismiss listener (if available) if (mCallback != null) { mCallback.onDismissed(this, event); } // Lastly, remove the view from the parent (if attached) final ViewParent parent = mView.getParent(); if (parent instanceof ViewGroup) { ((ViewGroup) parent).removeView(mView); }}/** * Returns true if we should animate the Snackbar view in/out. */private boolean shouldAnimate() { return !mAccessibilityManager.isEnabled();}/** * @return if the view is being being dragged or settled by {@link SwipeDismissBehavior}. */private boolean isBeingDragged() { final ViewGroup.LayoutParams lp = mView.getLayoutParams(); if (lp instanceof CoordinatorLayout.LayoutParams) { final CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) lp; final CoordinatorLayout.Behavior behavior = layoutParams.getBehavior(); if (behavior instanceof SwipeDismissBehavior) { return ((SwipeDismissBehavior) behavior).getDragState() != SwipeDismissBehavior.STATE_IDLE; } } return false;}public static class SnackbarLayout extends LinearLayout { private TextView mMessageView; private Button mActionView; private int mMaxWidth; private int mMaxInlineActionWidth; interface OnLayoutChangeListener { void onLayoutChange(View view, int left, int top, int right, int bottom); } interface OnAttachStateChangeListener { void onViewAttachedToWindow(View v); void onViewDetachedFromWindow(View v); } private OnLayoutChangeListener mOnLayoutChangeListener; private OnAttachStateChangeListener mOnAttachStateChangeListener; public SnackbarLayout(Context context) { this(context, null); } public SnackbarLayout(Context context, AttributeSet attrs) { super(context, attrs); 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); // 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. LayoutInflater.from(context).inflate(R.layout.view_tsnackbar_layout_include, this); ViewCompat.setAccessibilityLiveRegion(this, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE); ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); } @Override protected void onFinishInflate() { super.onFinishInflate(); mMessageView = (TextView) findViewById(R.id.snackbar_text); mActionView = (Button) findViewById(R.id.snackbar_action); } TextView getMessageView() { return mMessageView; } Button getActionView() { return mActionView; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mMaxWidth > 0 && getMeasuredWidth() > mMaxWidth) { widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } final int multiLineVPadding = getResources().getDimensionPixelSize( R.dimen.design_snackbar_padding_vertical_2lines); final int singleLineVPadding = getResources().getDimensionPixelSize( R.dimen.design_snackbar_padding_vertical); final boolean isMultiLine = mMessageView.getLayout().getLineCount() > 1; boolean remeasure = false; if (isMultiLine && mMaxInlineActionWidth > 0 && mActionView.getMeasuredWidth() > mMaxInlineActionWidth) { if (updateViewsWithinLayout(VERTICAL, multiLineVPadding, multiLineVPadding - singleLineVPadding)) { remeasure = true; } } else { final int messagePadding = isMultiLine ? multiLineVPadding : singleLineVPadding; if (updateViewsWithinLayout(HORIZONTAL, messagePadding, messagePadding)) { remeasure = true; } } if (remeasure) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } void animateChildrenIn(int delay, int duration) { ViewCompat.setAlpha(mMessageView, 0f); ViewCompat.animate(mMessageView).alpha(1f).setDuration(duration) .setStartDelay(delay).start(); if (mActionView.getVisibility() == VISIBLE) { ViewCompat.setAlpha(mActionView, 0f); ViewCompat.animate(mActionView).alpha(1f).setDuration(duration) .setStartDelay(delay).start(); } } void animateChildrenOut(int delay, int duration) { ViewCompat.setAlpha(mMessageView, 1f); ViewCompat.animate(mMessageView).alpha(0f).setDuration(duration) .setStartDelay(delay).start(); if (mActionView.getVisibility() == VISIBLE) { ViewCompat.setAlpha(mActionView, 1f); ViewCompat.animate(mActionView).alpha(0f).setDuration(duration) .setStartDelay(delay).start(); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (changed && mOnLayoutChangeListener != null) { mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b); } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (mOnAttachStateChangeListener != null) { mOnAttachStateChangeListener.onViewAttachedToWindow(this); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mOnAttachStateChangeListener != null) { mOnAttachStateChangeListener.onViewDetachedFromWindow(this); } } void setOnLayoutChangeListener(OnLayoutChangeListener onLayoutChangeListener) { mOnLayoutChangeListener = onLayoutChangeListener; } void setOnAttachStateChangeListener(OnAttachStateChangeListener listener) { mOnAttachStateChangeListener = listener; } private boolean updateViewsWithinLayout(final int orientation, final int messagePadTop, final int messagePadBottom) { boolean changed = false; if (orientation != getOrientation()) { setOrientation(orientation); changed = true; } if (mMessageView.getPaddingTop() != messagePadTop || mMessageView.getPaddingBottom() != messagePadBottom) { updateTopBottomPadding(mMessageView, messagePadTop, messagePadBottom); changed = true; } return changed; } private static void updateTopBottomPadding(View view, int topPadding, int bottomPadding) { if (ViewCompat.isPaddingRelative(view)) { ViewCompat.setPaddingRelative(view, ViewCompat.getPaddingStart(view), topPadding, ViewCompat.getPaddingEnd(view), bottomPadding); } else { view.setPadding(view.getPaddingLeft(), topPadding, view.getPaddingRight(), bottomPadding); } }}final class Behavior extends SwipeDismissBehavior<SnackbarLayout> { @Override public boolean canSwipeDismissView(View child) { return child instanceof SnackbarLayout; } @Override public boolean onInterceptTouchEvent(CoordinatorLayout parent, SnackbarLayout child, MotionEvent event) { // We want to make sure that we disable any Snackbar timeouts if the user is // currently touching the Snackbar. We restore the timeout when complete if (parent.isPointInChildBounds(child, (int) event.getX(), (int) event.getY())) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: SnackbarManager.getInstance().pauseTimeout(mManagerCallback); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: SnackbarManager.getInstance().restoreTimeoutIfPaused(mManagerCallback); break; } } return super.onInterceptTouchEvent(parent, child, event); }}
}
- SnackBar 源码bug修复、源码解析
- SnackBar源码解析
- Snackbar源码解析
- Alibaba-AndFix Bug热修复框架原理及源码解析
- Alibaba-AndFix Bug热修复框架原理及源码解析
- AndFix Bug热修复框架原理及源码解析
- AndFix Bug热修复框架原理及源码解析
- PhotoView源码初始化BUG修复
- Snackbar源码分析
- Snackbar新版Toast 从源码角度完全解析
- SnackBar源码解析-了解它的工作原理
- php源码阅读----php5.3.27 mysqli扩展bug及修复
- librdkafka 0.8 源码解析与bug分析
- Android-FixBug热修复框架的使用及源码分析(不发版修复bug)
- Android-FixBug热修复框架的使用及源码分析(不发版修复bug)
- 轻量级控件SnackBar使用以及源码分析
- SnackBar源码分析(来自design包)
- Android-Snackbar用法及源码分析
- keras实例
- Chapter01 Java开发入门——JDK的使用
- Retrofit2.0基础用法
- L2-003. 月饼
- freeswitch通话质量调查
- SnackBar 源码bug修复、源码解析
- 题目1209:最小邮票数DP
- jquery $.trim()方法使用介绍
- 无法收到AndroidStudio的更新提示
- 3月21日,TestFrame对象是事件源又是监听者,每日20行。
- 在Mac上基于XAMPP本地多站点的配置
- 【Linux】进程间通信(IPC)之信号量详解与测试用例
- Maven编译报错 Unknown lifecycle phase "mvn" 解决办法
- 每天一个Linux命令