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

来源:互联网 发布:泰瑞克埃文斯 数据 编辑:程序博客网 时间:2024/05/22 11:47

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

这里写图片描述

前言

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

今天,怀着不知名的心情,一起来分析下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

偌大的屋子 空无一人

原创粉丝点击