Snackbar source code parsing

Introduction

In May 2015, Google released Design Support Library, adding many components to support Material Design. It has been two years since then, and the version has changed from 22.2.0 to 26.0.0 Alpha 1. To understand the principle of control implementation, of course, from the simplest start, that is the main character of this article - Snackbar.

Basic use

  1. Only text prompts
Snackbar.make(view, "This is a message", Snackbar.LENGTH_LONG).show();
  1. A little click on the button
Snackbar.make(view, "This is a message", Snackbar.LENGTH_LONG)
        .setAction("UNDO", new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //TODO do something
            }
        })
        .show();

Of course, there are other attributes and methods, specific reference Google Official Documents.

Read with questions

  1. How is Snackbar added to the interface?
  2. How to modify the display location of Snackbar?
  3. Can Snackbar's layout be modified?
  4. How do multiple consecutive Snackbar s manage display?
  5. Why does Snackbar not block Floating Action Button when using Floating Action Button and SnackBar in Coordinator Layout?

Source code parsing

Source code is based on 25.3.0
Where should we start to interpret the source code? Of course, we started with SnackBar's most commonly used method. The first one we used was the make method.

make method

public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
            @Duration int duration) {
        final ViewGroup parent = findSuitableParent(view);
        if (parent == null) {
            throw new IllegalArgumentException("No suitable parent found from the given view. "
                    + "Please provide a valid view.");
        }

     ...
      // Later code omission
    }

There are two make methods in SnackBar, one is CharSequence and the other is Resouse id. Passing Resouse id eventually leads to the above approach.
Let's look at the first line of code in the method and call the findSuitableParent(View view) method. The code is as follows:

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

The code is small and the comments are clear. The purpose of this method is to lookup the upper view group of the view until the Coordinator Layout is found or the root layout is finished, and return to the view group found.
Root Layout: The layout of id for android.R.id.content is actually the parent ViewGroup of the layout that we set ContentView to write ourselves. The type is FrameLayout. You can learn about DecorView in detail.
Look back at Snackbar's make method:

public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
         @Duration int duration) {
     ....//The preceding code is omitted
     final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
     final SnackbarContentLayout content =
             (SnackbarContentLayout) inflater.inflate(
                     R.layout.design_layout_snackbar_include, parent, false);
     final Snackbar snackbar = new Snackbar(parent, content, content);
     snackbar.setText(text);
     snackbar.setDuration(duration);
     return snackbar;
 }

SnackBarContentLayout is actually a Linear Layout by inflate. Let's look at R.layout.design_layout_snackbar_include:

<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"
            android:textAlignment="viewStart"/>

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

Yes, that's Snackbar's main layout, a TextView and a Button.
The Snackbar Content Layout obtained is passed into Snackbar's construction method by instantiating Snackbar, and finally to Snackbar's parent BaseTransientBottomBar's construction method:

    protected BaseTransientBottomBar(@NonNull ViewGroup parent, @NonNull View content,
            @NonNull ContentViewCallback contentViewCallback) {
        ...
        //Omitting unimportant code
        mTargetParent = parent; //ViewGroup found by the previous findSuitableParent method
        //SnackbarContentLayout, which comes in from callback, implements the ContentViewCallback interface
        mContentViewCallback = contentViewCallback;
        mContext = parent.getContext();

        ThemeUtils.checkAppCompatTheme(mContext);

        LayoutInflater inflater = LayoutInflater.from(mContext);
        // Note that for backwards compatibility reasons we inflate a layout that is defined
        // in the extending Snackbar class. This is to prevent breakage of apps that have custom
        // coordinator layout behaviors that depend on that layout.
        mView = (SnackbarBaseLayout) inflater.inflate(
                R.layout.design_layout_snackbar, mTargetParent, false);
        mView.addView(content);//Add SnackbarContentLayout to SnackbarLayout
        ...//Eliminate the remaining code
    }

    /**
     * Returns the {@link BaseTransientBottomBar}'s view.
     */
    @NonNull
    public View getView() {
        return mView;
    }

Now that you have detailed notes, let's take a look at R.layout.design_layout_snackbar:

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

Notice the class, yes, this View is Snackbar Layout defined in Snackbar, inherited from Snackbar BaseLayout in BaseTransient Bottom Bar, and Snackbar BaseLayout from FrameLayout. In SnackbarLaout, only the onMeasure method has been restored, and other implementations are in SnackbarBaseLayout.
Another key point is that layout_gravity is set to bottom, which is why Snackbar is always displayed at the bottom.

So far we've learned that Snackbar's layout is actually a FrameLayout, and its content is a Linear Layout. BaseTransientBottomBar provides a getView method to obtain mView, which is Snackbar's root layout FrameLayout. Now that you can get the root layout, addView is definitely no problem in this layout. Problem 3 mentioned earlier can also be used to solve this problem.

show method

Why not action, but show? Because what I care about is how Snackbar displays.

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

See if this is a bit of a deception, how to Snackbar Manager show method, don't worry, let's first look at mManager Callback:

final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
        @Override
        public void show() {
            sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, BaseTransientBottomBar.this));
        }

        @Override
        public void dismiss(int event) {
            sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0,
                    BaseTransientBottomBar.this));
        }
    };

The mManager Callback uses Handler internally to control show and dismiss, and ultimately sHandler calls showView:

final void showView() {
    if (mView.getParent() == null) {
        final ViewGroup.LayoutParams lp = mView.getLayoutParams();

        if (lp instanceof CoordinatorLayout.LayoutParams) {
            // If LayoutParams is Coordinator Layout, set Behavior
            final CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp;

            final Behavior behavior = new Behavior();
            behavior.setStartAlphaSwipeDistance(0.1f);
            behavior.setEndAlphaSwipeDistance(0.6f);
            //Setting Swipe Dismiss Behavior, the specific function is to slide and delete view behavior. setSwipe Direction (Swipe Dismiss Behavior. SWIPE_DIRECTION_START_TO_END);
            behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
                @Override
                public void onDismiss(View view) {
                    view.setVisibility(View.GONE);
                    dispatchDismiss(BaseCallback.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, pause the timeout
                            SnackbarManager.getInstance().pauseTimeout(mManagerCallback);
                            break;
                        case SwipeDismissBehavior.STATE_IDLE:
                            // If the view has been released and is idle, restore the timeout
                            SnackbarManager.getInstance()
                                    .restoreTimeoutIfPaused(mManagerCallback);
                            break;
                    }
                }
            });
            clp.setBehavior(behavior);
            // Also set the inset edge so that views can dodge the bar correctly
            clp.insetEdge = Gravity.BOTTOM;
        }
        //The point is that mView is added to mTargetParent, which previously traversed the ViewGroup obtained from the view up.
        mTargetParent.addView(mView);
    }

    mView.setOnAttachStateChangeListener(
            new BaseTransientBottomBar.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(BaseCallback.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 BaseTransientBottomBar.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();
                }
            }
        });
    }
}

Through the code in showView, we finally understand how Snackbar is displayed. Snackbar is added directly to the mTargetParent, which is the parent Coordinator Layout or root layout of the View passed in by the make method.
According to Snackbar's layout file, we know that its layout_gravity is bottom, which is displayed at the bottom of mTargetParent. So can we just pass a Coordinator Layout with a fixed height to Snackbar's make and change Snackbar's display position? The answer is yes!
So here we have both Question 1 and Question 2. How should Question 4 be solved?

SnackbarManager

Based on the above analysis, we know that the show method will call SnackbarManager's show method, so let's take a look at the source code of SnackBarManager:

class SnackbarManager {

    static final int MSG_TIMEOUT = 0;

    private static final int SHORT_DURATION_MS = 1500;
    private static final int LONG_DURATION_MS = 2750;

    private static SnackbarManager sSnackbarManager;
    //Singleton pattern
    static SnackbarManager getInstance() {
        if (sSnackbarManager == null) {
            sSnackbarManager = new SnackbarManager();
        }
        return sSnackbarManager;
    }

    private final Object mLock;
    private final Handler mHandler;

    //duration and Callback for storing Snackbar currently displayed
    private SnackbarRecord mCurrentSnackbar;
    //duration and Callback for storing Snackbar to be displayed next
    private SnackbarRecord mNextSnackbar;

    private SnackbarManager() {
        mLock = new Object();
        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;
            }
        });
    }

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

    public void show(int duration, Callback callback) {
        synchronized (mLock) {
            if (isCurrentSnackbarLocked(callback)) { //Determine whether the Snackbar is currently displayed and update duration
                // 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);//Remove Callback to avoid memory leaks
                scheduleTimeoutLocked(mCurrentSnackbar);//Re-associate settings duration and allback
                return;
            } else if (isNextSnackbarLocked(callback)) { //Determine whether Snackbar is to be displayed next, update duration
                // 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();
            }
        }
    }

    public void dismiss(Callback callback, int event) {
        synchronized (mLock) {
            if (isCurrentSnackbarLocked(callback)) {
                cancelSnackbarLocked(mCurrentSnackbar, event);
            } else if (isNextSnackbarLocked(callback)) {
                cancelSnackbarLocked(mNextSnackbar, event);
            }
        }
    }

    /**
     * Should be called when a Snackbar is no longer displayed. This is after any exit
     * animation has finished.
     */
    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) {
                    showNextSnackbarLocked();
                }
            }
        }
    }

    /**
     * Should be called when a Snackbar is being shown. This is after any entrance animation has
     * finished.
     */
    public void onShown(Callback callback) {
        synchronized (mLock) {
            if (isCurrentSnackbarLocked(callback)) {
                scheduleTimeoutLocked(mCurrentSnackbar);
            }
        }
    }

    public void pauseTimeout(Callback callback) {
        synchronized (mLock) {
            if (isCurrentSnackbarLocked(callback) && !mCurrentSnackbar.paused) {
                mCurrentSnackbar.paused = true;
                mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
            }
        }
    }

    public void restoreTimeoutIfPaused(Callback callback) {
        synchronized (mLock) {
            if (isCurrentSnackbarLocked(callback) && mCurrentSnackbar.paused) {
                mCurrentSnackbar.paused = false;
                scheduleTimeoutLocked(mCurrentSnackbar);
            }
        }
    }

    public boolean isCurrent(Callback callback) {
        synchronized (mLock) {
            return isCurrentSnackbarLocked(callback);
        }
    }

    public boolean isCurrentOrNext(Callback callback) {
        synchronized (mLock) {
            return isCurrentSnackbarLocked(callback) || isNextSnackbarLocked(callback);
        }
    }

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

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

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

    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;
            }
        }
    }
    //Specific cancel method, callback callback dismiss
    private boolean cancelSnackbarLocked(SnackbarRecord record, int event) {
        final Callback callback = record.callback.get();
        if (callback != null) {
            // Make sure we remove any timeouts for the SnackbarRecord
            mHandler.removeCallbacksAndMessages(record);
            callback.dismiss(event);
            return true;
        }
        return false;
    }

    private boolean isCurrentSnackbarLocked(Callback callback) {
        return mCurrentSnackbar != null && mCurrentSnackbar.isSnackbar(callback);
    }

    private boolean isNextSnackbarLocked(Callback callback) {
        return mNextSnackbar != null && mNextSnackbar.isSnackbar(callback);
    }

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

    void handleTimeout(SnackbarRecord record) {
        synchronized (mLock) {
            if (mCurrentSnackbar == record || mNextSnackbar == record) {
                cancelSnackbarLocked(record, Snackbar.Callback.DISMISS_EVENT_TIMEOUT);
            }
        }
    }

}

In the singleton mode, two SnackbarRecord objects are held to store the duration and allback of the Snackbar currently displayed and subsequently displayed, that is to say, only two Snackbars are managed at most.
Looking at the show method, we can see that when mCurrent Snackbar is not null, the latter Snackbar will be stored in mNextSnackbar. Only when the currently displayed Snackbarduration arrives, call the onDismissed method, empty the mCurrentSnackbar, and then display the next Snackbar.
That is to say, when a Snackbar is displayed, other Snackbar show s are created many times, and when the Snackbar currently displayed ends, only the last Snackbar created will be displayed.
At this point, Question 4 is also clear. As for question 5, referring to Coordinator Layout Behavior, you can read another article of mine.—— Coordinator Layout Source Parsing.

Keywords: Android Google

Added by flyersman on Tue, 09 Jul 2019 23:51:53 +0300