此文章是我在簡書的文章,自行搬到開源中國.簡書地址:SweetTips: 快意靈動的Android提示庫!java
源碼及所在DEMO已上傳至GitHub:SweetTips,歡迎你們提Bug,喜歡的話記得Star或Fork下哈!android
##1.爲何要寫這個庫? 上面的問題也能夠這樣問:有哪些常見的需求,Android原生Toast及Design包中的Snackbar實現起來相對繁瑣? Toast:git
- 原生Toast沒法/不方便自定義顯示時間;
- 原生Toast,須要等待隊列中前面的Toast實例顯示完畢以後才能夠顯示,實時性差;
- 原生Toast,想在正在顯示的Toast實例上顯示新的內容並設置新內容的顯示時間,實現較繁瑣;
- 原生Toast,沒法/不方便自定義動畫;
- Android系統版本過多,不一樣的廠商對系統的定製也很不一樣,同一段代碼在不一樣的機器上,Toast的樣式差別很大,不利於App的一致性體驗;
Snackbar:github
- Design包中的Snackbar,沒法自定義動畫;
##2.SweetTips有什麼用? 很顯然,能夠解決上面列舉的那些很常見的小問題;ide
截圖:工具
##3.SweetTips的結構? 自定義Toast:SweetToast + 自定義Snackbar:SweetSnackbar + SnackbarUtils:SweetSnackbar的工具類post
##4.SweetTips的實現思路 SweetToast:動畫
- 在SweetToastManager中,利用隊列實現對SweetToast實例的管理,直接調用SweetToast的show()方法,能夠實現和原生Toast幾乎一致的體驗;
- 在SweetToastManager中,經過對隊列的清空,實現即時顯示當前SweetToast實例的內容;
- 在SweetToast中,經過設置WindowManager.LayoutParams.windowAnimations,實現SweetToast實例自定義的出入場動畫;
- SweetToast支持鏈式調用,調用盡量的快捷;
SweetSnackbar:ui
- 幾乎徹底拷貝了Design包中的Snackbar,只是添加了一個設置自定義出入場動畫的方法:setAnimations
- 參照以前寫過的一個工具類GitHub:SnackbarUtils,爲SweetSnackbar也寫了一個工具類,一樣支持練市調用,實現'一行代碼設置多重屬性';
SweetTips.javathis
- 這個工具類待完善,是爲了經過SweetToast或SweetSnackbar,封裝一些比較經常使用且精美的效果,經過靜態方法直接調用,提高開發者一些效率.
另外,爲了這個提示庫,也花了很多時間收集了一些經常使用的顏色,保存在Constant.java中,可做爲一個通用的工具類適用於不一樣項目,喜歡的同窗儘管拿走.
##5.SweetTips的使用限制 SweetToast是經過WindowManager向屏幕添加View來展現提示信息:
params.type = WindowManager.LayoutParams.TYPE_TOAST;
在Manifest.xml中已經聲明過權限:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
在SDK>=23(Android 6)的系統中,用戶須要手動容許當前App使用這個權限,才能夠正常顯示!
##6.SweetTips部分代碼
/** * 自定義Toast * * 做者:幻海流心 * GitHub:https://github.com/HuanHaiLiuXin * 郵箱:wall0920@163.com * 2016/12/13 */ public final class SweetToast { public static final int LENGTH_SHORT = 0; public static final int LENGTH_LONG = 1; public static final long SHORT_DELAY = 2000; // 2 seconds public static final long LONG_DELAY = 3500; // 3.5 seconds //SweetToast默認背景色 private static int mBackgroundColor = 0XE8484848; // private View mContentView = null; //內容區域View private SweetToastConfiguration mConfiguration = null; private WindowManager mWindowManager = null; private boolean showing = false; //是否在展現中 private boolean showEnabled = true; //是否容許展現 private boolean hideEnabled = true; //是否容許移除 private boolean stateChangeEnabled = true; //是否容許改變展現狀態 public static SweetToast makeText(Context context, CharSequence text){ return makeText(context, text, LENGTH_SHORT); } public static SweetToast makeText(View mContentView){ return makeText(mContentView, LENGTH_SHORT); } public static SweetToast makeText(Context context, CharSequence text, int duration) { try { LayoutInflater inflate = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View v = inflate.inflate(R.layout.transient_notification, null); TextView tv = (TextView)v.findViewById(R.id.message); tv.setText(text); SweetToast sweetToast = new SweetToast(); sweetToast.mContentView = v; sweetToast.mContentView.setBackgroundDrawable(getBackgroundDrawable(sweetToast, mBackgroundColor)); initConfiguration(sweetToast,duration); return sweetToast; }catch (Exception e){ Log.e("幻海流心","e:"+e.getLocalizedMessage()+":69"); } return null; } public static SweetToast makeText(View mContentView, int duration){ SweetToast sweetToast = new SweetToast(); sweetToast.mContentView = mContentView; initConfiguration(sweetToast,duration); return sweetToast; } private static void initConfiguration(SweetToast sweetToast,int duration){ try { if(duration < 0){ throw new RuntimeException("顯示時長必須>=0!"); } //1:初始化mWindowManager sweetToast.mWindowManager = (WindowManager) sweetToast.getContentView().getContext().getApplicationContext().getSystemService(Context.WINDOW_SERVICE); //2:初始化mConfiguration SweetToastConfiguration mConfiguration = new SweetToastConfiguration(); //2.1:設置顯示時間 mConfiguration.setDuration(duration); //2.2:設置WindowManager.LayoutParams屬性 WindowManager.LayoutParams params = new WindowManager.LayoutParams(); final Configuration config = sweetToast.getContentView().getContext().getResources().getConfiguration(); final int gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; params.gravity = gravity; if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { params.horizontalWeight = 1.0f; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { params.verticalWeight = 1.0f; } params.x = 0; params.y = sweetToast.getContentView().getContext().getResources().getDimensionPixelSize(R.dimen.toast_y_offset); params.verticalMargin = 0.0f; params.horizontalMargin = 0.0f; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.format = PixelFormat.TRANSLUCENT; params.windowAnimations = R.style.Anim_SweetToast; //在小米5S上實驗,前兩種type均會報錯 params.type = WindowManager.LayoutParams.TYPE_TOAST; // params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; // params.type = WindowManager.LayoutParams.TYPE_PHONE; params.setTitle("Toast"); params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; mConfiguration.setParams(params); sweetToast.setConfiguration(mConfiguration); }catch (Exception e){ Log.e("幻海流心","e:"+e.getLocalizedMessage()+":120"); } } /** * 根據指定的背景色,得到mToastView的背景drawable實例 * @param backgroundColor * @return */ private static ShapeDrawable getBackgroundDrawable(SweetToast sweetToast, @ColorInt int backgroundColor){ try { ShapeDrawable shapeDrawable = new ShapeDrawable(); DrawableCompat.setTint(shapeDrawable,backgroundColor); //獲取當前設備的屏幕尺寸 //實驗發現不一樣的設備上面,Toast內容區域的padding值並不相同,根據屏幕的寬度分別進行處理,儘可能接近設備原生Toast的體驗 int widthPixels = sweetToast.getContentView().getResources().getDisplayMetrics().widthPixels; int heightPixels = sweetToast.getContentView().getResources().getDisplayMetrics().heightPixels; float density = sweetToast.getContentView().getResources().getDisplayMetrics().density; if(widthPixels >= 1070){ //例如小米5S:1920 x 1080 shapeDrawable.setPadding((int)(density*13),(int)(density*12),(int)(density*13),(int)(density*12)); }else { //例如紅米2:1280x720 shapeDrawable.setPadding((int)(density*14),(int)(density*13),(int)(density*14),(int)(density*13)); } float radius = density*8; float[] outerRadii = new float[]{radius,radius,radius,radius,radius,radius,radius,radius}; int width = sweetToast.getContentView().getWidth(); int height = sweetToast.getContentView().getHeight(); RectF rectF = new RectF(1,1,width-1,height-1); RoundRectShape roundRectShape = new RoundRectShape(outerRadii,rectF,null); shapeDrawable.setShape(roundRectShape); DrawableCompat.setTint(shapeDrawable,backgroundColor); return shapeDrawable; }catch (Exception e){ Log.e("幻海流心","e:"+e.getLocalizedMessage()+":154"); } return null; } /** * 自定義SweetToast實例的入場出場動畫 * @param windowAnimations * @return */ public SweetToast setWindowAnimations(@StyleRes int windowAnimations){ mConfiguration.getParams().windowAnimations = windowAnimations; return this; } public SweetToast setGravity(int gravity, int xOffset, int yOffset) { mConfiguration.getParams().gravity = gravity; mConfiguration.getParams().x = xOffset; mConfiguration.getParams().y = yOffset; return this; } public SweetToast setMargin(float horizontalMargin, float verticalMargin) { mConfiguration.getParams().horizontalMargin = horizontalMargin; mConfiguration.getParams().verticalMargin = verticalMargin; return this; } /** * 向mContentView中添加View * * @param view * @param index * @return */ public SweetToast addView(View view, int index) { if(mContentView != null && mContentView instanceof ViewGroup){ ((ViewGroup)mContentView).addView(view,index); } return this; } /** * 設置SweetToast實例中TextView的文字顏色 * * @param messageColor * @return */ public SweetToast messageColor(@ColorInt int messageColor){ if(mContentView !=null && mContentView.findViewById(R.id.message) != null && mContentView.findViewById(R.id.message) instanceof TextView){ TextView textView = ((TextView) mContentView.findViewById(R.id.message)); textView.setTextColor(messageColor); } return this; } /** * 設置SweetToast實例的背景顏色 * * @param backgroundColor * @return */ public SweetToast backgroundColor(@ColorInt int backgroundColor){ if(mContentView!=null){ mContentView.setBackgroundDrawable(getBackgroundDrawable(this, backgroundColor)); } return this; } /** * 設置SweetToast實例的背景資源 * * @param background * @return */ public SweetToast backgroundResource(@DrawableRes int background){ if(mContentView!=null){ mContentView.setBackgroundResource(background); } return this; } /** * 設置SweetToast實例的文字顏色及背景顏色 * * @param messageColor * @param backgroundColor * @return */ public SweetToast colors(@ColorInt int messageColor, @ColorInt int backgroundColor) { messageColor(messageColor); backgroundColor(backgroundColor); return this; } /** * 設置SweetToast實例的文字顏色及背景資源 * * @param messageColor * @param background * @return */ public SweetToast textColorAndBackground(@ColorInt int messageColor, @DrawableRes int background) { messageColor(messageColor); backgroundResource(background); return this; } /** * 設置SweetToast實例的寬高 * 頗有用的功能,參考了簡書上的文章:http://www.jianshu.com/p/491b17281c0a * @param width SweetToast實例的寬度,單位是pix * @param height SweetToast實例的高度,單位是pix * @return */ public SweetToast size(int width, int height){ if(mContentView!=null && mContentView instanceof LinearLayout){ mContentView.setMinimumWidth(width); mContentView.setMinimumHeight(height); ((LinearLayout)mContentView).setGravity(Gravity.CENTER); try { TextView textView = ((TextView) mContentView.findViewById(R.id.message)); LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) textView.getLayoutParams(); params.width = LinearLayout.LayoutParams.MATCH_PARENT; params.height = LinearLayout.LayoutParams.MATCH_PARENT; textView.setLayoutParams(params); textView.setGravity(Gravity.CENTER); }catch (Exception e){ Log.e("幻海流心","e:"+e.getLocalizedMessage()); } } return this; } /** * 設置SweetToast實例的顯示位置:左上 * @return */ public SweetToast leftTop(){ return setGravity(Gravity.LEFT|Gravity.TOP,0,0); } /** * 設置SweetToast實例的顯示位置:右上 * @return */ public SweetToast rightTop(){ return setGravity(Gravity.RIGHT|Gravity.TOP,0,0); } /** * 設置SweetToast實例的顯示位置:左下 * @return */ public SweetToast leftBottom(){ return setGravity(Gravity.LEFT|Gravity.BOTTOM,0,0); } /** * 設置SweetToast實例的顯示位置:右下 * @return */ public SweetToast rightBottom(){ return setGravity(Gravity.RIGHT|Gravity.BOTTOM,0,0); } /** * 設置SweetToast實例的顯示位置:上中 * @return */ public SweetToast topCenter(){ return setGravity(Gravity.TOP|Gravity.CENTER_HORIZONTAL,0,0); } /** * 設置SweetToast實例的顯示位置:下中 * @return */ public SweetToast bottomCenter(){ return setGravity(Gravity.BOTTOM|Gravity.CENTER_HORIZONTAL,0,0); } /** * 設置SweetToast實例的顯示位置:左中 * @return */ public SweetToast leftCenter(){ return setGravity(Gravity.LEFT|Gravity.CENTER_VERTICAL,0,0); } /** * 設置SweetToast實例的顯示位置:右中 * @return */ public SweetToast rightCenter(){ return setGravity(Gravity.RIGHT|Gravity.CENTER_VERTICAL,0,0); } /** * 設置SweetToast實例的顯示位置:正中 * @return */ public SweetToast center(){ return setGravity(Gravity.CENTER,0,0); } /** * 將SweetToast實例顯示在指定View的頂部 * @param targetView 指定View * @param statusHeight 狀態欄顯示狀況下,狀態欄的高度 * @return */ public SweetToast layoutAbove(View targetView, int statusHeight){ if(mContentView!=null){ int[] locations = new int[2]; targetView.getLocationOnScreen(locations); //必須保證指定View的頂部可見 int screenHeight = ScreenUtil.getScreenHeight(mContentView.getContext()); if(locations[1] > statusHeight&&locations[1]<screenHeight){ setGravity(Gravity.BOTTOM|Gravity.CENTER_HORIZONTAL,0,screenHeight - locations[1]); } } return this; } /** * 將SweetToast實例顯示在指定View的底部 * @param targetView * @param statusHeight * @return */ public SweetToast layoutBellow(View targetView, int statusHeight){ if(mContentView!=null){ int[] locations = new int[2]; targetView.getLocationOnScreen(locations); //必須保證指定View的底部可見 int screenHeight = ScreenUtil.getScreenHeight(mContentView.getContext()); if(locations[1]+targetView.getHeight() > statusHeight&&locations[1]+targetView.getHeight()<screenHeight){ setGravity(Gravity.TOP|Gravity.CENTER_HORIZONTAL,0,locations[1]+targetView.getHeight()-statusHeight); } } return this; } /********************************************** SweetToast顯示及移除 **********************************************/ Handler mHandler = new Handler(); Runnable mHide = new Runnable() { @Override public void run() { handleHide(); } }; protected void handleHide() { if(this != null && mContentView != null){ if(stateChangeEnabled){ if(hideEnabled){ if(showing){ mWindowManager.removeView(mContentView); } showing = false; mContentView = null; }else{ } } } } protected void handleShow() { if(mContentView != null){ if(stateChangeEnabled){ if(showEnabled){ try { mWindowManager.addView(mContentView,mConfiguration.getParams()); long delay = (mConfiguration.getDuration() == LENGTH_LONG || mConfiguration.getDuration() == Toast.LENGTH_LONG) ? LONG_DELAY : ((mConfiguration.getDuration() == LENGTH_SHORT || mConfiguration.getDuration() == Toast.LENGTH_SHORT)? SHORT_DELAY : mConfiguration.getDuration()); mHandler.postDelayed(mHide,delay); showing = true; }catch (Exception e){ Log.e("幻海流心","e:"+e.getLocalizedMessage()+":213"); } } } } } /** * 保持當前實例的顯示狀態:不容許向Window中添加或者移除View */ protected void removeCallbacks(){ stateChangeEnabled = false; } /** * 設置是否容許展現當前實例 * @param showEnabled */ public void setShowEnabled(boolean showEnabled) { this.showEnabled = showEnabled; } /** * 設置是否容許移除當前實例中的View * @param hideEnabled */ public void setHideEnabled(boolean hideEnabled) { this.hideEnabled = hideEnabled; } /** * 設置是否容許改變當前實例的展現狀態 * @param stateChangeEnabled */ public void setStateChangeEnabled(boolean stateChangeEnabled) { this.stateChangeEnabled = stateChangeEnabled; } /** * 將當前實例添加到隊列{@link SweetToastManager#queue}中,若隊列爲空,則加入隊列後直接進行展現 */ public void show(){ try { if (Build.VERSION.SDK_INT >= 23) { //Android6.0以上,須要動態聲明權限 if(mContentView!=null && !Settings.canDrawOverlays(mContentView.getContext().getApplicationContext())) { //用戶還未容許該權限 Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); mContentView.getContext().startActivity(intent); return; } else if(mContentView!=null) { //用戶已經容許該權限 SweetToastManager.show(this); } } else { //Android6.0如下,不用動態聲明權限 if (mContentView!=null) { SweetToastManager.show(this); } } // SweetToastManager.show(this); }catch (Exception e){ Log.e("幻海流心","e:"+e.getLocalizedMessage()+":232"); } } /** * 利用隊列{@link SweetToastManager#queue}中正在展現的SweetToast實例,繼續展現當前實例的內容 */ public void showByPrevious(){ try { if (Build.VERSION.SDK_INT >= 23) { //Android6.0以上,須要動態聲明權限 if(mContentView!=null && !Settings.canDrawOverlays(mContentView.getContext().getApplicationContext())) { //用戶還未容許該權限 Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); mContentView.getContext().startActivity(intent); return; } else if(mContentView!=null) { //用戶已經容許該權限 SweetToastManager.showByPrevious(this); } } else { //Android6.0如下,不用動態聲明權限 if (mContentView!=null) { SweetToastManager.showByPrevious(this); } } // SweetToastManager.showByPrevious(this); }catch (Exception e){ Log.e("幻海流心","e:"+e.getLocalizedMessage()+":290"); } } /** * 清空隊列{@link SweetToastManager#queue}中已經存在的SweetToast實例,直接展現當前實例的內容 */ public void showImmediate(){ try { if (Build.VERSION.SDK_INT >= 23) { //Android6.0以上,須要動態聲明權限 if(mContentView!=null && !Settings.canDrawOverlays(mContentView.getContext().getApplicationContext())) { //用戶還未容許該權限 Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); mContentView.getContext().startActivity(intent); return; } else if(mContentView!=null) { //用戶已經容許該權限 SweetToastManager.showImmediate(this); } } else { //Android6.0如下,不用動態聲明權限 if (mContentView!=null) { SweetToastManager.showImmediate(this); } } // SweetToastManager.showImmediate(this); }catch (Exception e){ Log.e("幻海流心","e:"+e.getLocalizedMessage()+":252"); } } /** * 移除當前SweetToast並將mContentView置空 */ public void hide() { mHandler.post(mHide); } /********************************************** SweetToast顯示及移除 **********************************************/ //Setter&Getter public View getContentView() { return mContentView; } public void setContentView(View mContentView) { this.mContentView = mContentView; } public SweetToastConfiguration getConfiguration() { return mConfiguration; } public void setConfiguration(SweetToastConfiguration mConfiguration) { this.mConfiguration = mConfiguration; } public WindowManager getWindowManager() { return mWindowManager; } public void setWindowManager(WindowManager mWindowManager) { this.mWindowManager = mWindowManager; } public boolean isShowing() { return showing; } public void setShowing(boolean showing) { this.showing = showing; } }
源碼及所在DEMO已上傳至GitHub:SweetTips,歡迎你們提Bug,喜歡的話記得Star或Fork下哈!
That's all !