欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android Study Material Design 五 番外篇 之:深入分析SnackBar源码

程序员文章站 2022-05-30 22:17:41
...

LZ-Says:兜兜转转,似乎再次回到起点。。。

Android Study Material Design 五 番外篇 之:深入分析SnackBar源码

前言

一个人,还是会有些寂寥,孤僻。。。

今天,怀着不知名的心情,一起来分析下SnackBar源码,看看从源码中,我们能get到什么技能。

本文目标

通过源码的角度来了解谷歌大牛是如何Coding的。

分析

这里,我们主要分析SnackBar暴露的俩个部分,一为make(),二为show()。

make()分析

我们在上一篇文章中,简单的了解了SnackBar使用,通过解决部分答疑,让你不知不觉中get一项又一项技能。

SnackBar的易用,我们再次回顾下,示例化SnackBar,我们只需要调用make()方法即可,类似于Toast,今天我们就从它入手,看看人家是怎么玩的。

    @NonNull // 注解 非空
    public static Snackbar make(@NonNull View view, @NonNull CharSequence text, int duration) {
        // 初始化SnackBar
        Snackbar snackbar = new Snackbar(findSuitableParent(view));
        // 赋值
        snackbar.setText(text);
        // 设置显示时间
        snackbar.setDuration(duration);
        return snackbar;
    }

而接下来,我们看下@NonNull这里面又写了什么。

    /**
     * Denotes that a parameter, field or method return value can never be null.
     * <p>
     * This is a marker annotation and it has no specific attributes.
     */
    @Retention(CLASS)
    @Target({METHOD, PARAMETER, FIELD})
        public @interface NonNull {
    }

这里不得不说谷歌大牛,相当易读哈。can never be null,不能为空。

这里不知大家有没有注意到在初始化SnackBar中,还有一个神秘的家伙,还不知道它在搞什么?

    private static ViewGroup findSuitableParent(View view) {
        ViewGroup fallback = null;
        do {
            if(view instanceof CoordinatorLayout) { 
                return (ViewGroup)view; // 当前View属于CoordinatorLayout 直接返回
            }
            if(view instanceof FrameLayout) { // 当前View属于FrameLayout
                if(view.getId() == 16908290) { // id等于content内容 当前activity包含的view
                    return (ViewGroup)view;
                }
                fallback = (ViewGroup)view;
            }
            if(view != null) { 
                ViewParent parent = view.getParent(); // 获取根布局
                view = parent instanceof View?(View)parent:null; // 当前父布局属于view 直接return 否则return null
            }
        } while(view != null); // 一直在找根布局 找到最外层
        return fallback;
    }

由此可见,此方法的作用便是一直查找传入View的父布局,直到找到为止。

这里不得不说CoordinatorLayout,这家伙,是个好玩意,后期重点介绍下~

我们接下来看看SnackBar构造中又干了些什么鬼。

      private Snackbar(ViewGroup parent) {
        this.mParent = parent;
        this.mContext = parent.getContext(); 
        LayoutInflater inflater = LayoutInflater.from(this.mContext);
        // 渲染布局到View
        this.mView = (Snackbar.SnackbarLayout)inflater.inflate(layout.design_layout_snackbar, this.mParent, false);
    }

这里看到,谷歌大牛直接把SnackBar布局写死了。。。写死了。。。

关于inflate,这点LZ说明下,当最后参数为false时,此时布局是不会被加载到父布局中。当然,你可以理解为他们之间是独立的,这也就是为什么,当SnackBar弹出的时候,你可以选择与其交互,也可以继续你的操作的原因。

而关于为什么LZ知道,LZ这里放出源码内关键代码,大家一看便知:

    // We are supposed to attach all the views we found (int temp)
    // to root. Do that now.
    if (root != null && attachToRoot) {
        root.addView(temp, params);
    }

同时这里也用到synchronized机制,这点不得不说人家谷歌大牛思考确实挺全面,值得一学。

make()源码分析到此,接下来分析下show(),看看他是如何被Show出来的呢?

show()分析

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

点击进去可以看到关键点如下:

  • 首先,其内部通过SnackbarManager管理类去对Snackbar进行管理;

  • 其次,show的时候需要传入时间以及一个Callback

我们以此进行分解说明,一起探索未知世界。

    public void show(int duration, SnackbarManager.Callback callback) {
        Object var3 = this.mLock;
        synchronized(this.mLock) { // 这里再次用到同步锁 保证每次只有一个进行
            if(this.isCurrentSnackbar(callback)) {
                this.mCurrentSnackbar.duration = duration;
                // remove当前Callback
                // 这里通过handler进行消息分发 又是handler 看来改天得分析下handler了 
                this.mHandler.removeCallbacksAndMessages(this.mCurrentSnackbar);
                this.scheduleTimeoutLocked(this.mCurrentSnackbar);
            } else {
                if(this.isNextSnackbar(callback)) { // 这里大致猜测可能校验是否是下一个 如果是 设置时间 反之重新实例化
                    this.mNextSnackbar.duration = duration;
                } else {
                    this.mNextSnackbar = new SnackbarManager.SnackbarRecord(duration, callback);
                }
                // 这里相当于进行了回收操作 这点感觉很nice
                if(this.mCurrentSnackbar == null || !this.cancelSnackbarLocked(this.mCurrentSnackbar, 4)) {
                    this.mCurrentSnackbar = null;
                    this.showNextSnackbarLocked();
                }
            }
        }
    }

而实际分发中,还需要对用户传入显示值进行校验,校验通过后,开始分发消息。

    private void scheduleTimeoutLocked(SnackbarManager.SnackbarRecord r) {
        if(r.duration != -2) {
            int durationMs = 2750;
            // 如果大于0 便是用户设置值 反之设置默认
            if(r.duration > 0) {
                durationMs = r.duration;
            } else if(r.duration == -1) {
                durationMs = 1500;
            }
            // 移除
            this.mHandler.removeCallbacksAndMessages(r);
            // 分发
            this.mHandler.sendMessageDelayed(Message.obtain(this.mHandler, 0, r), (long)durationMs);
        }
    }

下面瞅瞅是如何通过Handler进行消息分发的,到底在搞什么?

    private final Handler mHandler = new Handler(Looper.getMainLooper(), new android.os.Handler.Callback() {
        public boolean handleMessage(Message message) {
            switch(message.what) {
            case 0: // 接收到hanlder消息进行处理
                SnackbarManager.this.handleTimeout((SnackbarManager.SnackbarRecord)message.obj);
                return true;
            default:
                return false;
            }
        }
    });

    private void handleTimeout(SnackbarManager.SnackbarRecord record) {
        Object var2 = this.mLock;
        synchronized(this.mLock) { // 同步锁
            if(this.mCurrentSnackbar == record || this.mNextSnackbar == record) {
                this.cancelSnackbarLocked(record, 2); // 关闭
            }
        }
    }

    private boolean cancelSnackbarLocked(SnackbarManager.SnackbarRecord record, int event) {
        // 获取到回调
        SnackbarManager.Callback callback = (SnackbarManager.Callback)record.callback.get();
        if(callback != null) {
            // dismiss
            callback.dismiss(event);
            return true;
        } else {
            return false;
        }
    }

看看人这套逻辑,思路,佩服。

噢,刚才遗漏一点,如下:

    private static class SnackbarRecord {

        // 弱引用 程序没有内存时 回收此类内存
        private final WeakReference<SnackbarManager.Callback> callback;
        private int duration;

        SnackbarRecord(int duration, SnackbarManager.Callback callback) {
            // 实例化 
            this.callback = new WeakReference(callback);
            this.duration = duration;
        }

        boolean isSnackbar(SnackbarManager.Callback callback) {
            return callback != null && this.callback.get() == callback;
        }
    }

大牛使用了弱引用,此类内存将在程序内存不足时自动回收~666

接下来我们关注一波文中常出现的Callback,看看这玩意是个什么鬼。

    private static class SnackbarRecord {

        // 弱引用 程序没有内存时 回收此类内存
        private final WeakReference<SnackbarManager.Callback> callback;
        private int duration;

        SnackbarRecord(int duration, SnackbarManager.Callback callback) {
            // 实例化 
            this.callback = new WeakReference(callback);
            this.duration = duration;
        }

        boolean isSnackbar(SnackbarManager.Callback callback) {
            return callback != null && this.callback.get() == callback;
        }
    }

提供显示以及dismiss俩种方式,正好配套。

还记得当我们调用show的时候,内部需要传入一个Callback么?感觉有关SnackBar真正显示的干货要来了。嘿嘿嘿~

    private final android.support.design.widget.SnackbarManager.Callback mManagerCallback = new android.support.design.widget.SnackbarManager.Callback() {
        public void show() {
            Snackbar.sHandler.sendMessage(Snackbar.sHandler.obtainMessage(0, Snackbar.this));
        }

        public void dismiss(int event) {
            Snackbar.sHandler.sendMessage(Snackbar.sHandler.obtainMessage(1, event, 0, Snackbar.this));
        }
    };

可以看到在实例化中,实现了俩个方法,而在显示的时候仅仅发了一个消息,一起来看看。

    private static final Handler sHandler = new Handler(Looper.getMainLooper(), new android.os.Handler.Callback() {
        public boolean handleMessage(Message message) {
            switch(message.what) {
            case 0:
                ((Snackbar)message.obj).showView(); // 显示
                return true;
            case 1:
                ((Snackbar)message.obj).hideView(message.arg1); // 隐藏
                return true;
            default:
                return false;
            }
        }
    });

    final void showView() {
        if(this.mView.getParent() == null) { // 当前view父容器为null
            LayoutParams lp = this.mView.getLayoutParams(); // 获取LayoutParams
            if(lp instanceof android.support.design.widget.CoordinatorLayout.LayoutParams) { // 如果包含
                Snackbar.Behavior behavior = new Snackbar.Behavior();
                // 设置透明度
                behavior.setStartAlphaSwipeDistance(0.1F);
                behavior.setEndAlphaSwipeDistance(0.6F);
                behavior.setSwipeDirection(0);
                // 设置监听
                behavior.setListener(new OnDismissListener() {
                    public void onDismiss(View view) {
                        Snackbar.this.dispatchDismiss(0);
                    }
                    public void onDragStateChanged(int state) {
                        switch(state) {
                        case 0:
                            SnackbarManager.getInstance().restoreTimeout(Snackbar.this.mManagerCallback);
                            break;
                        case 1:
                        case 2:
                            SnackbarManager.getInstance().cancelTimeout(Snackbar.this.mManagerCallback);
                        }
                    }
                });
                ((android.support.design.widget.CoordinatorLayout.LayoutParams)lp).setBehavior(behavior);
            }
            // 不管如何 直接添加到父布局 
            this.mParent.addView(this.mView);
        }
        // 如果没显示出来 那么就通过动画加载进来
        if(ViewCompat.isLaidOut(this.mView)) {
            this.animateViewIn();
        } else { // 否则就等待 启动时再次调用动画进入
            this.mView.setOnLayoutChangeListener(new Snackbar.SnackbarLayout.OnLayoutChangeListener() {
                public void onLayoutChange(View view, int left, int top, int right, int bottom) {
                    Snackbar.this.animateViewIn();
                    Snackbar.this.mView.setOnLayoutChangeListener((Snackbar.SnackbarLayout.OnLayoutChangeListener)null);
                }
            });
        }
    }

而有关动画,谷歌大拿也是废了脑细胞。

    private void animateViewIn() {
        if(VERSION.SDK_INT >= 14) { // 校验当前系统版本 因为属性动画
            ViewCompat.setTranslationY(this.mView, (float)this.mView.getHeight());
            ViewCompat.animate(this.mView).translationY(0.0F).setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR).setDuration(250L).setListener(new ViewPropertyAnimatorListenerAdapter() {
                public void onAnimationStart(View view) {
                    Snackbar.this.mView.animateChildrenIn(70, 180);
                }

                public void onAnimationEnd(View view) {
                    if(Snackbar.this.mCallback != null) {
                        Snackbar.this.mCallback.onShown(Snackbar.this);
                    }

                    SnackbarManager.getInstance().onShown(Snackbar.this.mManagerCallback);
                }
            }).start();
        } else {
            // 如果小于14,直接加载动画
            Animation anim = android.view.animation.AnimationUtils.loadAnimation(this.mView.getContext(), anim.design_snackbar_in);
            anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
            anim.setDuration(250L);
            anim.setAnimationListener(new AnimationListener() {
                public void onAnimationEnd(Animation animation) {
                    if(Snackbar.this.mCallback != null) {
                        Snackbar.this.mCallback.onShown(Snackbar.this);
                    }

                    SnackbarManager.getInstance().onShown(Snackbar.this.mManagerCallback);
                }

                public void onAnimationStart(Animation animation) {
                }

                public void onAnimationRepeat(Animation animation) {
                }
            });
            this.mView.startAnimation(anim);
        }

    }

看完之后只觉得相当Nice~!以后我们做这个的时候 也需要多加考虑,虽说目前市面主流差不多适配4.0就好,但是个别机型上还是会出现各种奇异问题。

同样在出来也就是消失的时候,也会有相应的动画呈现,具体大家可以参考文档进行参考学习了解即可。

接下来我们来看下有关其引用布局,看看能get什么技能。

<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" />
<!-- From: file:/usr/local/google/buildbot/repo_clients/https___googleplex-android.googlesource.com_a_platform_manifest.git/mnc-release/frameworks/support/design/res/layout/design_layout_snackbar.xml -->

这里可以了解到有关使用自定义View的俩种方法,如下:

  • 直接引用,当然此方法需要全包名+自定义View名;

  • 通过父节点为View,设置其class同样也可以。

在这里,我们还能学习到如何引用内部类,便是通过如下方式:

class="android.support.design.widget.Snackbar$SnackbarLayout"

这里大家如果看到LZ上一篇写的博文,可能会有疑问,LZ是如何知道它的id呢?很easy,下面直接贴出:

<?xml version="1.0" encoding="utf-8"?>
<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><!-- From: file:/usr/local/google/buildbot/repo_clients/https___googleplex-android.googlesource.com_a_platform_manifest.git/mnc-release/frameworks/support/design/res/layout/design_layout_snackbar_include.xml -->

而关于内部自定义View,简单了解下即可:

public static class SnackbarLayout extends LinearLayout

LinearLayout,控制content以及按钮。GG了

总结

1) 首先,明确SnackBar是与SnackBarManager配合使用;

2) 其次,内部采用Hanlder进行消息分发;

3) 随后,初始化时不断查找父布局,知道找到为止,由此可见,SnackBar外部依赖于CoordinatorLayout,而我们实际的布局的最终父节点是FrameLayout。查找完毕后通过一些操作直接添加布局;

4) 虽说布局写死了,但是我们能通过getView去设置其写死内部样式等等你想要的效果;

5) 通过同步锁,保证同一时间内只能有一个进行操作,通过了handler分发机制以及栈还有弱引用,使我们的View更加人性化。

当然,也不仅仅如上几点,具体大家还可以自行发掘~

End

真正孤身一人了。。。MMP

偌大的屋子 空无一人