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

来源:互联网 发布:scientific linux 6 编辑:程序博客网 时间:2024/06/05 09:12

概述:

  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

0 0