SnackBar源码解析-了解它的工作原理

来源:互联网 发布:上海钧珩大数据科技 编辑:程序博客网 时间:2024/05/03 15:58

Android Material Design中添加了许多东西,以前只是用一下(自己做demo玩,并没有用在实际项目中)。接下来的一系列文章,要给大家分析源码了,不知道你们高不高兴,开不开心,满不满足。

1.前言

Snackbar是一个控件,在屏幕底部快速弹出控件,与Toast不同的是,Snackbar支持相应用户操作。那么接下来就从源码的角度来告诉大家它的工作原理,方便大家更好的使用。
这篇博客涉及到的东西如下

  • Android消息机制
  • Snackbar.java
  • SnackbarManager.java
  • design_layout_snackbar.xml
  • design_layout_snackbar_include.xml
  • design包下的values.xml文件

2.SnackBar的创建过程

Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)        .setAction("Action", null).show();

接下来看SnackBar的make方法。

  public static Snackbar make(@NonNull View view,NonNull CharSequence text,@Duration int duration) {        Snackbar snackbar = newSnackbar(findSuitableParent(view));        snackbar.setText(text);        snackbar.setDuration(duration);        return snackbar;    }

我们来看下newSnackbar方法,看这个之前,我们要先了解了解下 findSuitableParent方法

 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;    }

从上面的代码可以看出,直到返回CoordinatorLayout或者是decorview中的content部分(通过setContentView设置的那个啦)。

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);    }

这个方法中,检查主题,加载布局等等。我们接下来看看加载布局的具体操作,在SnackbarLayout中。

3.布局的加载及初始化过程

在SnackLayout的构造方法中,做了如下操作,我们来一一说明。

3.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));        }
3.2 布局加载

这里的属性不在多介绍,我们重点看下下面这句话

    LayoutInflater.from(context).inflate(R.layout.design_layout_snackbar_include, this);

结合上下文,就是将design_layout_snackbar_include.xml文件的布局加载到design_layout_snackbar.xml文件中。(因为使用了merge标签)
我们分别来看看这2个文件

<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"  style="@style/Widget.Design.Snackbar" />

而上面布局中的style是在design包中values文件中定义好的一个style,包括高度、背景色等属性,代码如下:

<style name="Widget.Design.Snackbar" parent="android:Widget">        <item name="android:theme">@style/ThemeOverlay.AppCompat.Dark</item>        <item name="android:minWidth">@dimen/design_snackbar_min_width</item>        <item name="android:maxWidth">@dimen/design_snackbar_max_width</item>        <item name="android:background">@drawable/design_snackbar_background</item>        <item name="android:paddingLeft">@dimen/design_snackbar_padding_horizontal</item>        <item name="android:paddingRight">@dimen/design_snackbar_padding_horizontal</item>        <item name="elevation">@dimen/design_snackbar_elevation</item>        <item name="maxActionInlineWidth">@dimen/design_snackbar_action_inline_max_width</item>    </style>

最重要的design_layout_snackbar_include.xml文件来了;

<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> 

这里很重要,我们可以通过上面那2个ID去获取到view,然后改变view的属性,去定制我们的SnackBar。

4.运行过程

我们从最开始的show方法入手。

public void show() {        SnackbarManager.getInstance().show(mDuration, mManagerCallback);    }

其中mDuration是我们设置的时长。

    private 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));        }    };

这个对象,用来发送消息来控制显示和取消的。(消息传递)

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();            }        }    }

上面的判断步骤如下:

  • 如果是当前正在显示的SnackBar对应的CallBack
    • 更新显示时长
    • 从消息队列中移除
    • scheduleTimeoutLocked()发送定时消息
  • 如果是下一个要显示的
    • 更新显示时长
  • 都不是 就创建一个SnackbarRecord对象

上面代码中第二部分解释如下

  • 如果已经有在显示的了,直接返回
  • 没有的话 显示下一个。

在这里要说一下,mCurrentSnackbar 和mNextSnackbar 有点绕

  • mCurrentSnackbar 当前正在显示的
  • 和mNextSnackbar 下一个要显示的

要想知道Snackbar的显示和隐藏的调用过程。我们从上面的showNextSnackbarLocked方法看起。

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;            }        }    }

在这里我们能看到会调用callback的show方法,而这个calllback对象就是我们在调用snackbar的show方法是传进去的那个。
在callback的show方法中,发送了一个MSG_SHOW的消息。那么我们就跟踪下这里的Hanlder了。

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();                        return true;                    case MSG_DISMISS:                        ((Snackbar) message.obj).hideView(message.arg1);                        return true;                }                return false;            }        });    }
  • 看到这里简单粗暴的调用了showview方法。这个showview方法是我们想要的。由于这里代码比较场,就不贴代码了,简单的介绍下过程。

showview方法的逻辑

  • 判断当前view的parent是否为null
    • 为null,当我们的view 的LayoutParams instanceof CoordinatorLayout.LayoutParams 的时候,设置behavior。
  • 设置OnAttachStateChangeListener 监听器
  • 是否在屏幕之外
    • 是,动画进入
    • 不是,setOnLayoutChangeListener,在layout改变的时候动画。

到这里我们就完成了显示过程。其中很重要的一个方法为animateViewIn。这里会根据版本去选择合适的动画类型,并且,隐藏部分的逻辑也在这里,不信,你们继续看。
在animateViewIn中有下面这一句代码。

SnackbarManager.getInstance().onShown(mManagerCallback);

我们看看onShow方法干了什么。

 public void onShown(Callback callback) {        synchronized (mLock) {            if (isCurrentSnackbarLocked(callback)) {                scheduleTimeoutLocked(mCurrentSnackbar);            }        }    }
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);        mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_TIMEOUT, r), durationMs);    }

可以很明显的看到,在上面发送了一个叫做MSG_TIMEOUT的消息,继续追终,最后会到达cancelSnackbarLocked方法。

private boolean cancelSnackbarLocked(SnackbarRecord record, int event) {        final Callback callback = record.callback.get();        if (callback != null) {            callback.dismiss(event);            return true;        }        return false;    }

恩,绕一大圈,终于看到了dismiss方法。那么,接下来的代码我们就不看了。

1 0
原创粉丝点击