• Android SnackBar:你值得拥有的信息提示控件


    概述:

      Snackbar提供了一个介于Toast和AlertDialog之间轻量级控件,它可以很方便的提供消息的提示和动作反馈。

      有时我们想这样一种控件,我们想他可以想Toast一样显示完成便可以消失,又想在这个信息提示上进行用户反馈。写Toast没有反馈效果,写Dialog只能点击去dismiss它。是的,可能你会说是可以去自定义它们来达到这样的效果。而事实上也是这样。


    实现:

      其实要实现这样的一个提示窗口,只是针对自定义控件来说,应该是So easy的,不过这里我们想着会有一些比较完善的功能,比如,我们要同时去显示多个提示时,又该如何呢?这一点我们就要去模仿Toast原本的队列机制了。

      对于本博客的源码也并非本人所写,我也只是在网络上下载下来之后研究了一下,并把研究的一些过程在这里和大家分享一下。代码的xml部分,本文不做介绍,大家可以在源码中去详细了解。

      而在Java的部分,则有三个类。这三个类的功能职责则是依据MVC的模式来编写,看完这三个类,自己也是学到了不少的东西呢。M(Snack)、V(SnackContainer)、C(SnackBar)


    M(Snack)

    /**
     * Model角色,显示SnackBar时信息属性
     * http://blog.csdn.net/lemon_tree12138
     */
    class Snack implements Parcelable {
    
        final String mMessage;
    
        final String mActionMessage;
    
        final int mActionIcon;
    
        final Parcelable mToken;
    
        final short mDuration;
    
        final ColorStateList mBtnTextColor;
    
        Snack(String message, String actionMessage, int actionIcon,
                Parcelable token, short duration, ColorStateList textColor) {
            mMessage = message;
            mActionMessage = actionMessage;
            mActionIcon = actionIcon;
            mToken = token;
            mDuration = duration;
            mBtnTextColor = textColor;
        }
    
        // reads data from parcel
        Snack(Parcel p) {
            mMessage = p.readString();
            mActionMessage = p.readString();
            mActionIcon = p.readInt();
            mToken = p.readParcelable(p.getClass().getClassLoader());
            mDuration = (short) p.readInt();
            mBtnTextColor = p.readParcelable(p.getClass().getClassLoader());
        }
    
        // writes data to parcel
        public void writeToParcel(Parcel out, int flags) {
            out.writeString(mMessage);
            out.writeString(mActionMessage);
            out.writeInt(mActionIcon);
            out.writeParcelable(mToken, 0);
            out.writeInt((int) mDuration);
            out.writeParcelable(mBtnTextColor, 0);
        }
    
        public int describeContents() {
            return 0;
        }
    
        // creates snack array
        public static final Parcelable.Creator<Snack> CREATOR = new Parcelable.Creator<Snack>() {
            public Snack createFromParcel(Parcel in) {
                return new Snack(in);
            }
    
            public Snack[] newArray(int size) {
                return new Snack[size];
            }
        };
    }
      这一个类就没什么好说的了,不过也有一点还是要注意一下的。就是这个类需要去实现Parcelable的接口。为什么呢?因为我们在V(SnackContainer)层会对M(Snack)在Bundle之间进行传递,而在Bundle和Intent之间的数据传递时,如果是一个类的对象,那么这个对象要是Parcelable或是Serializable类型的。


    V(SnackContainer)

    class SnackContainer extends FrameLayout {
    
        private static final int ANIMATION_DURATION = 300;
    
        private static final String SAVED_MSGS = "SAVED_MSGS";
    
        private Queue<SnackHolder> mSnacks = new LinkedList<SnackHolder>();
    
        private AnimationSet mOutAnimationSet;
        private AnimationSet mInAnimationSet;
    
        private float mPreviousY;
    
        public SnackContainer(Context context) {
            super(context);
            init();
        }
    
        public SnackContainer(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        SnackContainer(ViewGroup container) {
            super(container.getContext());
    
            container.addView(this, new ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT));
            setVisibility(View.GONE);
            setId(R.id.snackContainer);
            init();
        }
    
        private void init() {
            mInAnimationSet = new AnimationSet(false);
    
            TranslateAnimation mSlideInAnimation = new TranslateAnimation(
                    TranslateAnimation.RELATIVE_TO_PARENT, 0.0f,
                    TranslateAnimation.RELATIVE_TO_PARENT, 0.0f,
                    TranslateAnimation.RELATIVE_TO_SELF, 1.0f,
                    TranslateAnimation.RELATIVE_TO_SELF, 0.0f);
    
            AlphaAnimation mFadeInAnimation = new AlphaAnimation(0.0f, 1.0f);
    
            mInAnimationSet.addAnimation(mSlideInAnimation);
            mInAnimationSet.addAnimation(mFadeInAnimation);
    
            mOutAnimationSet = new AnimationSet(false);
    
            TranslateAnimation mSlideOutAnimation = new TranslateAnimation(
                    TranslateAnimation.RELATIVE_TO_PARENT, 0.0f,
                    TranslateAnimation.RELATIVE_TO_PARENT, 0.0f,
                    TranslateAnimation.RELATIVE_TO_SELF, 0.0f,
                    TranslateAnimation.RELATIVE_TO_SELF, 1.0f);
    
            AlphaAnimation mFadeOutAnimation = new AlphaAnimation(1.0f, 0.0f);
    
            mOutAnimationSet.addAnimation(mSlideOutAnimation);
            mOutAnimationSet.addAnimation(mFadeOutAnimation);
    
            mOutAnimationSet.setDuration(ANIMATION_DURATION);
            mOutAnimationSet
                    .setAnimationListener(new Animation.AnimationListener() {
                        @Override
                        public void onAnimationStart(Animation animation) {
    
                        }
    
                        @Override
                        public void onAnimationEnd(Animation animation) {
                            removeAllViews();
    
                            if (!mSnacks.isEmpty()) {
                                sendOnHide(mSnacks.poll());
                            }
    
                            if (!isEmpty()) {
                                showSnack(mSnacks.peek());
                            } else {
                                setVisibility(View.GONE);
                            }
                        }
    
                        @Override
                        public void onAnimationRepeat(Animation animation) {
    
                        }
                    });
        }
    
        @Override
        protected void onDetachedFromWindow() {
            super.onDetachedFromWindow();
            mInAnimationSet.cancel();
            mOutAnimationSet.cancel();
            removeCallbacks(mHideRunnable);
            mSnacks.clear();
        }
    
        /**
         * Q Management
         */
    
        public boolean isEmpty() {
            return mSnacks.isEmpty();
        }
    
        public Snack peek() {
            return mSnacks.peek().snack;
        }
    
        public Snack pollSnack() {
            return mSnacks.poll().snack;
        }
    
        public void clearSnacks(boolean animate) {
            mSnacks.clear();
            if (animate) {
                mHideRunnable.run();
            }
        }
    
        /**
         * Showing Logic
         */
    
        public boolean isShowing() {
            return !mSnacks.isEmpty();
        }
    
        public void hide() {
            removeCallbacks(mHideRunnable);
            mHideRunnable.run();
        }
    
        public void showSnack(Snack snack, View snackView,
                OnVisibilityChangeListener listener) {
            showSnack(snack, snackView, listener, false);
        }
    
        public void showSnack(Snack snack, View snackView,
                OnVisibilityChangeListener listener, boolean immediately) {
            if (snackView.getParent() != null && snackView.getParent() != this) {
                ((ViewGroup) snackView.getParent()).removeView(snackView);
            }
    
            SnackHolder holder = new SnackHolder(snack, snackView, listener);
            mSnacks.offer(holder);
            if (mSnacks.size() == 1) {
                showSnack(holder, immediately);
            }
        }
    
        private void showSnack(final SnackHolder holder) {
            showSnack(holder, false);
        }
    
        /**
         * TODO
         * 2015年7月19日
         * 上午4:24:10
         */
        private void showSnack(final SnackHolder holder, boolean showImmediately) {
    
            setVisibility(View.VISIBLE);
    
            sendOnShow(holder);
    
            addView(holder.snackView);
            holder.messageView.setText(holder.snack.mMessage);
            if (holder.snack.mActionMessage != null) {
                holder.button.setVisibility(View.VISIBLE);
                holder.button.setText(holder.snack.mActionMessage);
                holder.button.setCompoundDrawablesWithIntrinsicBounds(
                        holder.snack.mActionIcon, 0, 0, 0);
            } else {
                holder.button.setVisibility(View.GONE);
            }
    
            holder.button.setTextColor(holder.snack.mBtnTextColor);
    
            if (showImmediately) {
                mInAnimationSet.setDuration(0);
            } else {
                mInAnimationSet.setDuration(ANIMATION_DURATION);
            }
            startAnimation(mInAnimationSet);
    
            if (holder.snack.mDuration > 0) {
                postDelayed(mHideRunnable, holder.snack.mDuration);
            }
    
            holder.snackView.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    float y = event.getY();
    
                    switch (event.getAction()) {
                    case MotionEvent.ACTION_MOVE:
                        int[] location = new int[2];
                        holder.snackView.getLocationInWindow(location);
                        if (y > mPreviousY) {
                            float dy = y - mPreviousY;
                            holder.snackView.offsetTopAndBottom(Math.round(4 * dy));
    
                            if ((getResources().getDisplayMetrics().heightPixels - location[1]) - 100 <= 0) {
                                removeCallbacks(mHideRunnable);
                                sendOnHide(holder);
                                startAnimation(mOutAnimationSet);
    
                                // 清空列表中的SnackHolder,也可以不要这句话。这样如果后面还有SnackBar要显示就不会被Hide掉了。
                                if (!mSnacks.isEmpty()) {
                                    mSnacks.clear();
                                }
                            }
                        }
                    }
    
                    mPreviousY = y;
    
                    return true;
                }
            });
        }
    
        private void sendOnHide(SnackHolder snackHolder) {
            if (snackHolder.visListener != null) {
                snackHolder.visListener.onHide(mSnacks.size());
            }
        }
    
        private void sendOnShow(SnackHolder snackHolder) {
            if (snackHolder.visListener != null) {
                snackHolder.visListener.onShow(mSnacks.size());
            }
        }
    
        /**
         * Runnable stuff
         */
        private final Runnable mHideRunnable = new Runnable() {
            @Override
            public void run() {
                if (View.VISIBLE == getVisibility()) {
                    startAnimation(mOutAnimationSet);
                }
            }
        };
    
        /**
         * Restoration
         */
        public void restoreState(Bundle state, View v) {
            Parcelable[] messages = state.getParcelableArray(SAVED_MSGS);
            boolean showImmediately = true;
    
            for (Parcelable message : messages) {
                showSnack((Snack) message, v, null, showImmediately);
                showImmediately = false;
            }
        }
    
        public Bundle saveState() {
            Bundle outState = new Bundle();
    
            final int count = mSnacks.size();
            final Snack[] snacks = new Snack[count];
            int i = 0;
            for (SnackHolder holder : mSnacks) {
                snacks[i++] = holder.snack;
            }
    
            outState.putParcelableArray(SAVED_MSGS, snacks);
            return outState;
        }
    
        private static class SnackHolder {
            final View snackView;
            final TextView messageView;
            final TextView button;
    
            final Snack snack;
            final OnVisibilityChangeListener visListener;
    
            private SnackHolder(Snack snack, View snackView,
                    OnVisibilityChangeListener listener) {
                this.snackView = snackView;
                button = (TextView) snackView.findViewById(R.id.snackButton);
                messageView = (TextView) snackView.findViewById(R.id.snackMessage);
    
                this.snack = snack;
                visListener = listener;
            }
        }
    }
      这是要显示我们View的地方。这里的SnackContainer一看名称就应该知道它是一个容器类了吧,我们把得到将Show的SnackBar都放进一个Queue里,需要显示哪一个就把在Queue中取出显示即可。而它本身就好像是一面墙,我们会把一个日历挂在上面,显示过一张就poll掉一个,直到Queue为Empty为止。

      在上面的显示SnackBar的代码showSnack(...)部分,我们看到还有一个onTouch的触摸事件。好了,代码中实现的是当我们把这个SnackBar向下Move的时候,这一条SnackBar就被Hide了,而要不要再继续显示Queue中其他的SnackBar就要针对具体的需求自己来衡量了。

      SnackContainer中还有一个SnackHolder的内部类,大家可以把它看成是Adapter中的ViewHolder,很类似的东西。


    C(SnackBar)

    public class SnackBar {
    
        public static final short LONG_SNACK = 5000;
    
        public static final short MED_SNACK = 3500;
    
        public static final short SHORT_SNACK = 2000;
    
        public static final short PERMANENT_SNACK = 0;
    
        private SnackContainer mSnackContainer;
    
        private View mParentView;
    
        private OnMessageClickListener mClickListener;
    
        private OnVisibilityChangeListener mVisibilityChangeListener;
    
        public interface OnMessageClickListener {
            void onMessageClick(Parcelable token);
        }
    
        public interface OnVisibilityChangeListener {
    
            /**
             * Gets called when a message is shown
             * 
             * @param stackSize
             *            the number of messages left to show
             */
            void onShow(int stackSize);
    
            /**
             * Gets called when a message is hidden
             * 
             * @param stackSize
             *            the number of messages left to show
             */
            void onHide(int stackSize);
        }
    
        public SnackBar(Activity activity) {
            ViewGroup container = (ViewGroup) activity.findViewById(android.R.id.content);
            View v = activity.getLayoutInflater().inflate(R.layout.sb_snack, container, false);
            
    //        v.setBackgroundColor(activity.getResources().getColor(R.color.beige));
            
            init(container, v);
        }
    
        public SnackBar(Context context, View v) {
            LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            inflater.inflate(R.layout.sb_snack_container, ((ViewGroup) v));
            View snackLayout = inflater.inflate(R.layout.sb_snack, ((ViewGroup) v), false);
            init((ViewGroup) v, snackLayout);
        }
    
        private void init(ViewGroup container, View v) {
            mSnackContainer = (SnackContainer) container.findViewById(R.id.snackContainer);
            if (mSnackContainer == null) {
                mSnackContainer = new SnackContainer(container);
            }
    
            mParentView = v;
            TextView snackBtn = (TextView) v.findViewById(R.id.snackButton);
            snackBtn.setOnClickListener(mButtonListener);
        }
    
        public static class Builder {
    
            private SnackBar mSnackBar;
    
            private Context mContext;
            private String mMessage;
            private String mActionMessage;
            private int mActionIcon = 0;
            private Parcelable mToken;
            private short mDuration = MED_SNACK;
            private ColorStateList mTextColor;
    
            /**
             * Constructs a new SnackBar
             * 
             * @param activity
             *            the activity to inflate into
             */
            public Builder(Activity activity) {
                mContext = activity.getApplicationContext();
                mSnackBar = new SnackBar(activity);
            }
    
            /**
             * Constructs a new SnackBar
             * 
             * @param context
             *            the context used to obtain resources
             * @param v
             *            the view to inflate the SnackBar into
             */
            public Builder(Context context, View v) {
                mContext = context;
                mSnackBar = new SnackBar(context, v);
            }
    
            /**
             * Sets the message to display on the SnackBar
             * 
             * @param message
             *            the literal string to display
             * @return this builder
             */
            public Builder withMessage(String message) {
                mMessage = message;
                return this;
            }
    
            /**
             * Sets the message to display on the SnackBar
             * 
             * @param messageId
             *            the resource id of the string to display
             * @return this builder
             */
            public Builder withMessageId(int messageId) {
                mMessage = mContext.getString(messageId);
                return this;
            }
    
            /**
             * Sets the message to display as the action message
             * 
             * @param actionMessage
             *            the literal string to display
             * @return this builder
             */
            public Builder withActionMessage(String actionMessage) {
                mActionMessage = actionMessage;
                return this;
            }
    
            /**
             * Sets the message to display as the action message
             * 
             * @param actionMessageResId
             *            the resource id of the string to display
             * @return this builder
             */
            public Builder withActionMessageId(int actionMessageResId) {
                if (actionMessageResId > 0) {
                    mActionMessage = mContext.getString(actionMessageResId);
                }
    
                return this;
            }
    
            /**
             * Sets the action icon
             * 
             * @param id
             *            the resource id of the icon to display
             * @return this builder
             */
            public Builder withActionIconId(int id) {
                mActionIcon = id;
                return this;
            }
    
            /**
             * Sets the {@link com.github.mrengineer13.snackbar.SnackBar.Style} for
             * the action message
             * 
             * @param style
             *            the
             *            {@link com.github.mrengineer13.snackbar.SnackBar.Style} to
             *            use
             * @return this builder
             */
            public Builder withStyle(Style style) {
                mTextColor = getActionTextColor(style);
                return this;
            }
    
            /**
             * The token used to restore the SnackBar state
             * 
             * @param token
             *            the parcelable containing the saved SnackBar
             * @return this builder
             */
            public Builder withToken(Parcelable token) {
                mToken = token;
                return this;
            }
    
            /**
             * Sets the duration to show the message
             * 
             * @param duration
             *            the number of milliseconds to show the message
             * @return this builder
             */
            public Builder withDuration(Short duration) {
                mDuration = duration;
                return this;
            }
    
            /**
             * Sets the {@link android.content.res.ColorStateList} for the action
             * message
             * 
             * @param colorId
             *            the
             * @return this builder
             */
            public Builder withTextColorId(int colorId) {
                ColorStateList color = mContext.getResources().getColorStateList(colorId);
                mTextColor = color;
                return this;
            }
    
            /**
             * Sets the OnClickListener for the action button
             * 
             * @param onClickListener
             *            the listener to inform of click events
             * @return this builder
             */
            public Builder withOnClickListener(
                    OnMessageClickListener onClickListener) {
                mSnackBar.setOnClickListener(onClickListener);
                return this;
            }
    
            /**
             * Sets the visibilityChangeListener for the SnackBar
             * 
             * @param visibilityChangeListener
             *            the listener to inform of visibility changes
             * @return this builder
             */
            public Builder withVisibilityChangeListener(
                    OnVisibilityChangeListener visibilityChangeListener) {
                mSnackBar.setOnVisibilityChangeListener(visibilityChangeListener);
                return this;
            }
    
            /**
             * Shows the first message in the SnackBar
             * 
             * @return the SnackBar
             */
            public SnackBar show() {
                Snack message = new Snack(mMessage,
                        (mActionMessage != null ? mActionMessage.toUpperCase()
                                : null), mActionIcon, mToken, mDuration,
                        mTextColor != null ? mTextColor
                                : getActionTextColor(Style.DEFAULT));
    
                mSnackBar.showMessage(message);
    
                return mSnackBar;
            }
    
            private ColorStateList getActionTextColor(Style style) {
                switch (style) {
                case ALERT:
                    return mContext.getResources().getColorStateList(
                            R.color.sb_button_text_color_red);
                case INFO:
                    return mContext.getResources().getColorStateList(
                            R.color.sb_button_text_color_yellow);
                case CONFIRM:
                    return mContext.getResources().getColorStateList(
                            R.color.sb_button_text_color_green);
                case DEFAULT:
                    return mContext.getResources().getColorStateList(
                            R.color.sb_default_button_text_color);
                default:
                    return mContext.getResources().getColorStateList(
                            R.color.sb_default_button_text_color);
                }
            }
        }
    
        private void showMessage(Snack message) {
            mSnackContainer.showSnack(message, mParentView, mVisibilityChangeListener);
        }
    
        /**
         * Calculates the height of the SnackBar
         * 
         * @return the height of the SnackBar
         */
        public int getHeight() {
            mParentView.measure(View.MeasureSpec.makeMeasureSpec(
                    mParentView.getWidth(), View.MeasureSpec.EXACTLY),
                    View.MeasureSpec.makeMeasureSpec(mParentView.getHeight(),
                            View.MeasureSpec.AT_MOST));
            return mParentView.getMeasuredHeight();
        }
    
        /**
         * Getter for the SnackBars parent view
         * 
         * @return the parent view
         */
        public View getContainerView() {
            return mParentView;
        }
    
        private final View.OnClickListener mButtonListener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mClickListener != null && mSnackContainer.isShowing()) {
                    mClickListener.onMessageClick(mSnackContainer.peek().mToken);
                }
                mSnackContainer.hide();
            }
        };
    
        private SnackBar setOnClickListener(OnMessageClickListener listener) {
            mClickListener = listener;
            return this;
        }
    
        private SnackBar setOnVisibilityChangeListener(
                OnVisibilityChangeListener listener) {
            mVisibilityChangeListener = listener;
            return this;
        }
    
        /**
         * Clears all of the queued messages
         * 
         * @param animate
         *            whether or not to animate the messages being hidden
         */
        public void clear(boolean animate) {
            mSnackContainer.clearSnacks(animate);
        }
    
        /**
         * Clears all of the queued messages
         * 
         */
        public void clear() {
            clear(true);
        }
    
        /**
         * All snacks will be restored using the view from this Snackbar
         */
        public void onRestoreInstanceState(Bundle state) {
            mSnackContainer.restoreState(state, mParentView);
        }
    
        public Bundle onSaveInstanceState() {
            return mSnackContainer.saveState();
        }
    
        public enum Style {
            DEFAULT, ALERT, CONFIRM, INFO
        }
    }
      相信如果你写过自定义的Dialog,对这个类一定不会陌生,它采用的是Builder模式编写,这样在使用端的部分就可以很轻松地设置它们。就像这样:

    mBuilder = new SnackBar.Builder(MainActivity.this).withMessage("Hello SnackBar!").withDuration(SnackBar.LONG_SNACK);
                    mBuilder = mBuilder.withActionMessage("Undo");
                    mBuilder = mBuilder.withStyle(SnackBar.Style.INFO);
                    mBuilder = mBuilder.withOnClickListener(new OnMessageClickListener() {
                        
                        @Override
                        public void onMessageClick(Parcelable token) {
                            Toast.makeText(getApplicationContext(), "Click Undo", 0).show();
                        }
                    });
                    mSnackBar = mBuilder.show();


    效果图:


    不带Action按钮的SnackBar



    带Action按钮的SnackBar


    源码下载:

    http://download.csdn.net/detail/u013761665/8906571

  • 相关阅读:
    c/c++:字符串输入输出流
    POJ 1036Gangsters【DP】
    POJ 1157LITTLE SHOP OF FLOWERS【DP】
    一个月后....
    http://poj.org/problem?id=1258
    POJ 2677 Tour【DP】
    POJ 1160Post Office【DP】
    C基础
    linux面试fork函数题
    linux学习
  • 原文地址:https://www.cnblogs.com/fengju/p/6336067.html
Copyright © 2020-2023  润新知