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