編輯:關於Android編程
Snackbar提供了一個介於Toast和AlertDialog之間輕量級控件,它可以很方便的提供消息的提示和動作反饋。
有時我們想這樣一種控件,我們想他可以想Toast一樣顯示完成便可以消失,又想在這個信息提示上進行用戶反饋。寫Toast沒有反饋效果,寫Dialog只能點擊去dismiss它。是的,可能你會說是可以去自定義它們來達到這樣的效果。而事實上也是這樣。
其實要實現這樣的一個提示窗口,只是針對自定義控件來說,應該是So easy的,不過這裡我們想著會有一些比較完善的功能,比如,我們要同時去顯示多個提示時,又該如何呢?這一點我們就要去模仿Toast原本的隊列機制了。
對於本博客的源碼也並非本人所寫,我也只是在網絡上下載下來之後研究了一下,並把研究的一些過程在這裡和大家分享一下。代碼的xml部分,本文不做介紹,大家可以在源碼中去詳細了解。
而在Java的部分,則有三個類。這三個類的功能職責則是依據MVC的模式來編寫,看完這三個類,自己也是學到了不少的東西呢。M(Snack)、V(SnackContainer)、C(SnackBar)
/** * 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這一個類就沒什麼好說的了,不過也有一點還是要注意一下的。就是這個類需要去實現Parcelable的接口。為什麼呢?因為我們在V(SnackContainer)層會對M(Snack)在Bundle之間進行傳遞,而在Bundle和Intent之間的數據傳遞時,如果是一個類的對象,那麼這個對象要是Parcelable或是Serializable類型的。CREATOR = new Parcelable.Creator () { public Snack createFromParcel(Parcel in) { return new Snack(in); } public Snack[] newArray(int size) { return new Snack[size]; } }; }
class SnackContainer extends FrameLayout { private static final int ANIMATION_DURATION = 300; private static final String SAVED_MSGS = SAVED_MSGS; private Queue這是要顯示我們View的地方。這裡的SnackContainer一看名稱就應該知道它是一個容器類了吧,我們把得到將Show的SnackBar都放進一個Queue裡,需要顯示哪一個就把在Queue中取出顯示即可。而它本身就好像是一面牆,我們會把一個日歷掛在上面,顯示過一張就poll掉一個,直到Queue為Empty為止。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; } } }
在上面的顯示SnackBar的代碼showSnack(...)部分,我們看到還有一個onTouch的觸摸事件。好了,代碼中實現的是當我們把這個SnackBar向下Move的時候,這一條SnackBar就被Hide了,而要不要再繼續顯示Queue中其他的SnackBar就要針對具體的需求自己來衡量了。
SnackContainer中還有一個SnackHolder的內部類,大家可以把它看成是Adapter中的ViewHolder,很類似的東西。
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.在移動設備訪問m.alipay.com時,如果本地安裝了支付寶客戶端,則浏覽器會調用本地客戶端,沒有安裝則會跳轉到下載頁面,提示安裝。剛好有這樣的需求,就分析了下支付
本節引言: 嘿嘿,假如你們公司是做HTML5端的移動APP的,就是通過WebView來顯示網頁的,假如你訪問的網頁 不存在,或者其他錯誤,報404,401,4
微信朋友圈值乎怎麼玩?大家是否還記得微信紅包看照片的活動?現在知乎也推出了一個新的活動,那就是如果你想看完我的整篇文章,請付錢。 哈哈~是不是很有意思呢?
安裝SVN服務端安裝VisualSVN-Server,我的電腦是XP,所以安裝的VisualSVN-Server-2.5.8.msi版本。 這裡要注