轻量级控件SnackBar使用以及源码分析

本篇博客将会给大家带来一个轻量级控件SnackBar,为什么要讲SnackBar?Snackbar:的提出实际上是界于Toast和Dialog的中间产物。因为Toast与Dialog各有一定的不足,使用Toast的时候, 用户无法交互;使用Dialog:用户可以交互,但是体验会打折扣,会阻断用户的连贯性操作;但是使用Snackbar既可以做到轻量级的用户提醒效果,又可以有交互的功能,本博客将会从SnackBar的使用和源码分析两个方面进行介绍。

SnackBar的使用

SnackBar的使用十分简单,其实和Toast的使用方法差不多,我们写一个很简单的例子,来看一下SnackBar的使用,布局上有一个按钮,点击后弹出SnackBar,弹出的逻辑如下,布局代码很简单就不贴了。

public void showSnackBar(View view) {
    //LENGTH_INDEFINITE:无穷
    Snackbar snackbar = Snackbar.make(view,"您的Wifi已经开启!",Snackbar.LENGTH_INDEFINITE);
    snackbar.setAction("确定", new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Toast.makeText(MainActivity.this, "确定啦", Toast.LENGTH_SHORT).show();
        }
    });
    snackbar.setCallback(new Snackbar.Callback() {
        @Override
        public void onDismissed(Snackbar snackbar, int event) {
            Toast.makeText(MainActivity.this, "SnackBar消失了", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onShown(Snackbar snackbar) {
            Toast.makeText(MainActivity.this, "SnackBar出现了", Toast.LENGTH_SHORT).show();
        }
    });
    snackbar.setActionTextColor(Color.BLUE);
    snackbar.show();
}

可以看到上面代码,setAction方法用于给SnackBar设置按钮,setCallback方法用于设置回调,当SnackBar出现时或者消失时都会有相应的回调,同时setActionTextColor方法可以给改变SnackBar中按钮的颜色。


SnackBar的源码分析

SnackBar是通过make方法进行创建的,所以我们首先需要查看SnackBar的make方法

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

里面有一个findSuitableParent方法,Snackbar内部把view传递给了这个方法,查看该方法的逻辑

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

发现这里竟然是一个do while的循环,只要view!= null,就会一直循环下去,里面会对view进行判断,是CoordinatorLayout,则直接返回,如果是FrameLayout,并且当view.getId() == android.R.id.content时候,也将view进行返回,大家都知道R.id.content就是decorView下的content部分,否则就会将这个view赋值给fallback,这个fallback就是一个viewGroup。下面这一句非常关键

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

取出view的Parent并且只要这个parent是View,就将其赋值给我门的view,到这里我们明白了,这个死循环就是为了无限的从传进来的这个view开始无限的向上寻找view的父亲,直到没有父亲为止,最后会返回fallback。然后我们自然会先去查看Snackbar构造函数,看它里面是进行了什么逻辑

private Snackbar(ViewGroup parent) {
        mParent = parent;
        mContext = parent.getContext();

        LayoutInflater inflater = LayoutInflater.from(mContext);
        mView = (SnackbarLayout) inflater.inflate(R.layout.design_layout_snackbar, mParent, false);
    }

在这里面最重要的一句就是渲染了一个R.layout.design_layout_snackbar的布局,很明显这个布局是系统自带的,很明显在这里已经写死了,所以我们想修改这个SnackBar显然是不行的,而且它还强转成了SnackbarLayout布局,我们可以查看一下这个布局的代码,这个布局在design包的layout下

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

在这里我们可以学到2点,一是如何引用某个类里面的内部类,就是通过class=“”,第二点就是自定义控件的第二种引用方法,使用View标签,然后内部使用class进行引用。我们看一下SnackbarLayout的代码:

<pre name="code" class="java"><pre name="code" class="java">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.design_layout_snackbar_include, this);
        }


 
 

里面会创建一个TypedArray,然后取出里面的属性进行设置,最后会渲染一个布局:R.layout.design_layout_snackbar_include,它被渲染到当前SnackbarLayout之中

<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/snackbar_padding_vertical"
            android:paddingBottom="@dimen/snackbar_padding_vertical"
            android:paddingLeft="@dimen/snackbar_padding_horizontal"
            android:paddingRight="@dimen/snackbar_padding_horizontal"
            android:textAppearance="@style/TextAppearance.Design.Snackbar.Message"
            android:maxLines="@integer/snackbar_text_max_lines"
            android:layout_gravity="center_vertical|left|start"
            android:ellipsize="end"/>

    <TextView
            android:id="@+id/snackbar_action"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/snackbar_extra_spacing_horizontal"
            android:layout_marginStart="@dimen/snackbar_extra_spacing_horizontal"
            android:layout_gravity="center_vertical|right|end"
            android:background="?attr/selectableItemBackground"
            android:paddingTop="@dimen/snackbar_padding_vertical"
            android:paddingBottom="@dimen/snackbar_padding_vertical"
            android:paddingLeft="@dimen/snackbar_padding_horizontal"
            android:paddingRight="@dimen/snackbar_padding_horizontal"
            android:visibility="gone"
            android:textAppearance="@style/TextAppearance.Design.Snackbar.Action"/>

</merge>

Snackbar的布局里面果然是使用了这个布局,如果我们要改变布局的样式,我们就修改这个文件里面的相关属性就可以了,就比如这里的textAppearance。我们回到Snackbar的构造方法中,同时它还把parent传了进去,  看过LayoutInflater源码的都知道,只有同时满足root不为空,而且attachToRoot为真的时候,root才会去添加这个渲染的temp,也就是我们上面传进来的R.layout.design_layout_snackbar,明显没有添加进mParent中去,那么Snackbar到底是在哪里addView的呢?我们一定要去追寻出这个添加Snackbar的地方。

if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

我们跟踪mView这个变量,终于在showView方法中,找到了addView的足迹

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) {
                        dispatchDismiss(Callback.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, cancel the timeout
                                SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
                                break;
                            case SwipeDismissBehavior.STATE_IDLE:
                                // If the view has been released and is idle, restore the timeout
                                SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
                                break;
                        }
                    }
                });
                ((CoordinatorLayout.LayoutParams) lp).setBehavior(behavior);
            }

            mParent.addView(mView);
        }

        if (ViewCompat.isLaidOut(mView)) {
            // If the view is already laid out, animate it now
            animateViewIn();
        } else {
            // Otherwise, add one of our layout change listeners and animate it in when laid out
            mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {
                @Override
                public void onLayoutChange(View view, int left, int top, int right, int bottom) {
                    animateViewIn();
                    mView.setOnLayoutChangeListener(null);
                }
            });
        }
    }

这里的代码比较长,我们一点一点进行分析,当mView.getParent() == null时,就是mView已经没有父View的时候,会取出它的LayoutParams,如果这个LayoutParams instanceofCoordinatorLayout.LayoutParams,然后是new一个Behavior,给Behavior设置各种参数以及监听,最后这个Behavior会设置给LayoutParams,然后这个mView最终会添加mParent的ViewGroup容器之中。

当view已经绘制完毕后,会给它设置一个出现的动画animateViewIn,否则会给mView设置布局变化的监听,每一次布局改变都会调用动画,并把监听设置为null,这里设置为null也是非常巧妙的,如果不这样设置,这个监听就会一直回调。

我们粗略查看一下animateViewIn的内部逻辑:

private void animateViewIn() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            ViewCompat.setTranslationY(mView, mView.getHeight());
            ViewCompat.animate(mView).translationY(0f)
                    .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR)
                    .setDuration(ANIMATION_DURATION)
                    .setListener(new ViewPropertyAnimatorListenerAdapter() {
                        @Override
                        public void onAnimationStart(View view) {
                            mView.animateChildrenIn(ANIMATION_DURATION - ANIMATION_FADE_DURATION,
                                    ANIMATION_FADE_DURATION);
                        }

                        @Override
                        public void onAnimationEnd(View view) {
                            if (mCallback != null) {
                                mCallback.onShown(Snackbar.this);
                            }
                            SnackbarManager.getInstance().onShown(mManagerCallback);
                        }
                    }).start();
        } else {
            Animation anim = AnimationUtils.loadAnimation(mView.getContext(), R.anim.design_snackbar_in);
            anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
            anim.setDuration(ANIMATION_DURATION);
            anim.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationEnd(Animation animation) {
                    if (mCallback != null) {
                        mCallback.onShown(Snackbar.this);
                    }
                    SnackbarManager.getInstance().onShown(mManagerCallback);
                }

                @Override
                public void onAnimationStart(Animation animation) {}

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

其实就是进行判断,如果编译的版本大于3.0,就是用属性动画进行一系列的动画设置,否则就是用传统的动画设置。

接着我们查看一下Show方法的逻辑:

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

这里用到了SnackbarManager,我们查看一下它的源码,看到getInstance就知道它肯定使用了单例的设计模式

static SnackbarManager getInstance() {
        if (sSnackbarManager == null) {
            sSnackbarManager = new SnackbarManager();
        }
        return sSnackbarManager;
    }

直接查看show方法

        synchronized (mLock) {
            if (isCurrentSnackbar(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 (isNextSnackbar(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();
            }
        }
    }

Show方法中会传进来一个callback,这个callback是一个接口,里面有两个抽象方法show和dismiss

interface Callback {
        void show();
        void dismiss(int event);
}

再回到show方法内部,可以发现首先是加了一个同步锁,这样的目的,我们也可以猜出来,就是防止多次对SnackBar调用show方法,只有当一个SnackBar show完事了之后,下一个SnackBar才能show,也可以看出来SnackbarManager是对SnackBar起到管理作用的。通过isCurrentSnackbar(callback)方法判断传入show方法的callback是否在队列之中,其中有一个SnackbarRecord类型的变量mCurrentSnackbar用于记录时间。

if (isCurrentSnackbar(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;
            }

如果当前的Snackbar已经展示完毕,同时它的展示时间已经到了,mHandler就会发送一个消息,移除这个Snackbar的callback,同时调用scheduleTimeoutLocked方法,我们查看一下该方法的内部逻辑:

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

首先是根据给SnackBar设置的不同显示时长来进行相应处理,然后是调用mHandler的removeCallbacksAndMessages和sendMessageDelayed方法,进行消息的发送,接着我们可以看一下handler做了什么处理

mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
            @Override
            public boolean handleMessage(Message message) {
                switch (message.what) {
                    case MSG_TIMEOUT:
                        handleTimeout((SnackbarRecord) message.obj);
                        return true;
                }
                return false;
            }
        });

当时间到了,会调用handleTimeout方法,SnackbarRecord会被传入这个方法之中

private void handleTimeout(SnackbarRecord record) {
        synchronized (mLock) {
            if (mCurrentSnackbar == record || mNextSnackbar == record) {
                cancelSnackbarLocked(record, Snackbar.Callback.DISMISS_EVENT_TIMEOUT);
            }
        }
}
在handleTimeout中同样会同步的调用cancelSnackbarLocked方法
private boolean cancelSnackbarLocked(SnackbarRecord record, int event) {
        final Callback callback = record.callback.get();
        if (callback != null) {
            callback.dismiss(event);
            return true;
        }
        return false;
}

这方法内部会从SnackbarRecord内部把callback取出来,如果callback不为空的时候,会调用callback的dismiss方法,回到show方法中,如果调用show方法的是下一个Snackbar就更新一下mNextSnackbar的duration,否则就new 一个SnackbarRecord。

接下来是判定,如果当前有一个Snackbar,就不做处理。


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

如果当前SnackbarRecord不为空,而且其中的callback正在dismiss时,return,否则会清空当前snackbar,然后展示下一个snackbar

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

showNextSnackbarLocked其中的逻辑也很简单,把下一个SnackbarRecord赋值给当前的,取出里面的callback,不为空时调用show方法。我们再查看一下SnackbarRecord的源码:

private static class SnackbarRecord {
        private final WeakReference<Callback> callback;
        private int duration;

        SnackbarRecord(int duration, Callback callback) {
            this.callback = new WeakReference<>(callback);
            this.duration = duration;
        }

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

里面使用了一个弱引用来包裹callback,这里是很值得我们学习的,使用WeakReference可以较好的避免内存泄漏的问题。Callback我们之前说过是一个接口,我们需要找一下它的实现类,既然是在show方法中把callback传进来的,所以我们要寻找一下SnackBarManager的show方法是在哪里调用的。本篇之前我们就看过SnackBar的show方法,里面调用了SnackbarManager的show方法

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

该方法内的参数mManagerCallback就是SnackBarManager内部Callback的实现类

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

可以发现,其内部实现show与dismiss方法,使用sHandler发送不同的消息,查看sHandler的实现

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

当message为MSG_SHOW时,会调用Snackbar的showView方法,当message为MSG_DISMISS时,会调用Snackbar的hideView,showView方法内部逻辑我们之前已经分析过了,再看一下hideView方法:

final void hideView(int event) {
        if (mView.getVisibility() != View.VISIBLE || isBeingDragged()) {
            onViewHidden(event);
        } else {
            animateViewOut(event);
        }
    }

hideView方法内调用onViewHidden方法:

private void onViewHidden(int event) {
        // First remove the view from the parent
        mParent.removeView(mView);
        // Now call the dismiss listener (if available)
        if (mCallback != null) {
            mCallback.onDismissed(this, event);
        }
        // Finally, tell the SnackbarManager that it has been dismissed
        SnackbarManager.getInstance().onDismissed(mManagerCallback);
}

首先mParent会把mView进行移除,然后如果mCallback!= null,会调用mCallback的onDismissed方法,最后调用SnackbarManager的onDismissed的方法,将callback移除出队列,到这里SnackBar和SnackbarManager的源码我们就基本分析完毕了。


























评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

进击的代码家

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值