概述:
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