Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android SnackBar:你值得擁有的信息提示控件

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

編輯:關於Android編程

概述:

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 CREATOR = new Parcelable.Creator() {
        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 mSnacks = new LinkedList();

    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

 

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved