SweetTips: 快意灵动的Android提示库!

此文章是我在简书的文章,自行搬到博客园.简书地址:SweetTips: 快意灵动的Android提示库!

源码及所在DEMO已上传至GitHub:SweetTips,欢迎大家提Bug,喜欢的话记得Star或Fork下哈!

1.为什么要写这个库?

上面的问题也可以这样问:有哪些常见的需求,Android原生Toast及Design包中的Snackbar实现起来相对繁琐?Toast:

  1. 原生Toast无法/不方便自定义显示时间;
  2. 原生Toast,需要等待队列中前面的Toast实例显示完毕之后才可以显示,实时性差;
  3. 原生Toast,想在正在显示的Toast实例上显示新的内容并设置新内容的显示时间,实现较繁琐;
  4. 原生Toast,无法/不方便自定义动画;
  5. Android系统版本过多,不同的厂商对系统的定制也很不同,同一段代码在不同的机器上,Toast的样式差异很大,不利于App的一致性体验;

Snackbar:

  • Design包中的Snackbar,无法自定义动画;

2.SweetTips有什么用?

很显然,可以解决上面列举的那些很常见的小问题;

截图:

3.SweetTips的结构?

自定义Toast:SweetToast + 自定义Snackbar:SweetSnackbar + SnackbarUtils:SweetSnackbar的工具类

4.SweetTips的实现思路

SweetToast:

  • 在SweetToastManager中,利用队列实现对SweetToast实例的管理,直接调用SweetToast的show()方法,可以实现和原生Toast几乎一致的体验;
  • 在SweetToastManager中,通过对队列的清空,实现即时显示当前SweetToast实例的内容;
  • 在SweetToast中,通过设置WindowManager.LayoutParams.windowAnimations,实现SweetToast实例自定义的出入场动画;
  • SweetToast支持链式调用,调用尽可能的快捷;

SweetSnackbar:

  • 几乎完全拷贝了Design包中的Snackbar,只是添加了一个设置自定义出入场动画的方法:setAnimations
  • 参照之前写过的一个工具类GitHub:SnackbarUtils,为SweetSnackbar也写了一个工具类,同样支持练市调用,实现'一行代码设置多重属性';

SweetTips.java

  • 这个工具类待完善,是为了通过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部分代码

  1 /**
  2  * 自定义Toast
  3  *
  4  * 作者:幻海流心
  5  * GitHub:https://github.com/HuanHaiLiuXin
  6  * 邮箱:wall0920@163.com
  7  * 2016/12/13
  8  */
  9 
 10 public final class SweetToast {
 11     public static final int LENGTH_SHORT = 0;
 12     public static final int LENGTH_LONG = 1;
 13     public static final long SHORT_DELAY = 2000; // 2 seconds
 14     public static final long LONG_DELAY = 3500; // 3.5 seconds
 15     //SweetToast默认背景色
 16     private static int mBackgroundColor = 0XE8484848;
 17     //
 18     private View mContentView = null;   //内容区域View
 19     private SweetToastConfiguration mConfiguration = null;
 20     private WindowManager mWindowManager = null;
 21     private boolean showing = false;    //是否在展示中
 22     private boolean showEnabled = true; //是否允许展示
 23     private boolean hideEnabled = true; //是否允许移除
 24     private boolean stateChangeEnabled = true;  //是否允许改变展示状态
 25 
 26     public static SweetToast makeText(Context context, CharSequence text){
 27         return makeText(context, text, LENGTH_SHORT);
 28     }
 29     public static SweetToast makeText(View mContentView){
 30         return makeText(mContentView, LENGTH_SHORT);
 31     }
 32     public static SweetToast makeText(Context context, CharSequence text, int duration) {
 33         try {
 34             LayoutInflater inflate = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
 35             View v = inflate.inflate(R.layout.transient_notification, null);
 36             TextView tv = (TextView)v.findViewById(R.id.message);
 37             tv.setText(text);
 38             SweetToast sweetToast = new SweetToast();
 39             sweetToast.mContentView = v;
 40             sweetToast.mContentView.setBackgroundDrawable(getBackgroundDrawable(sweetToast, mBackgroundColor));
 41             initConfiguration(sweetToast,duration);
 42             return sweetToast;
 43         }catch (Exception e){
 44             Log.e("幻海流心","e:"+e.getLocalizedMessage()+":69");
 45         }
 46         return null;
 47     }
 48     public static SweetToast makeText(View mContentView, int duration){
 49         SweetToast sweetToast = new SweetToast();
 50         sweetToast.mContentView = mContentView;
 51         initConfiguration(sweetToast,duration);
 52         return sweetToast;
 53     }
 54     private static void initConfiguration(SweetToast sweetToast,int duration){
 55         try {
 56             if(duration < 0){
 57                 throw new RuntimeException("显示时长必须>=0!");
 58             }
 59             //1:初始化mWindowManager
 60             sweetToast.mWindowManager = (WindowManager) sweetToast.getContentView().getContext().getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
 61             //2:初始化mConfiguration
 62             SweetToastConfiguration mConfiguration = new SweetToastConfiguration();
 63             //2.1:设置显示时间
 64             mConfiguration.setDuration(duration);
 65             //2.2:设置WindowManager.LayoutParams属性
 66             WindowManager.LayoutParams params = new WindowManager.LayoutParams();
 67             final Configuration config = sweetToast.getContentView().getContext().getResources().getConfiguration();
 68             final int gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
 69             params.gravity = gravity;
 70             if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
 71                 params.horizontalWeight = 1.0f;
 72             }
 73             if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
 74                 params.verticalWeight = 1.0f;
 75             }
 76             params.x = 0;
 77             params.y = sweetToast.getContentView().getContext().getResources().getDimensionPixelSize(R.dimen.toast_y_offset);
 78             params.verticalMargin = 0.0f;
 79             params.horizontalMargin = 0.0f;
 80             params.height = WindowManager.LayoutParams.WRAP_CONTENT;
 81             params.width = WindowManager.LayoutParams.WRAP_CONTENT;
 82             params.format = PixelFormat.TRANSLUCENT;
 83             params.windowAnimations = R.style.Anim_SweetToast;
 84             //在小米5S上实验,前两种type均会报错
 85             params.type = WindowManager.LayoutParams.TYPE_TOAST;
 86 //            params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
 87 //            params.type = WindowManager.LayoutParams.TYPE_PHONE;
 88             params.setTitle("Toast");
 89             params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
 90                     | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
 91                     | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
 92             mConfiguration.setParams(params);
 93             sweetToast.setConfiguration(mConfiguration);
 94         }catch (Exception e){
 95             Log.e("幻海流心","e:"+e.getLocalizedMessage()+":120");
 96         }
 97     }
 98     /**
 99      * 根据指定的背景色,获得mToastView的背景drawable实例
100      * @param backgroundColor
101      * @return
102      */
103     private static ShapeDrawable getBackgroundDrawable(SweetToast sweetToast, @ColorInt int backgroundColor){
104         try {
105             ShapeDrawable shapeDrawable = new ShapeDrawable();
106             DrawableCompat.setTint(shapeDrawable,backgroundColor);
107             //获取当前设备的屏幕尺寸
108             //实验发现不同的设备上面,Toast内容区域的padding值并不相同,根据屏幕的宽度分别进行处理,尽量接近设备原生Toast的体验
109             int widthPixels = sweetToast.getContentView().getResources().getDisplayMetrics().widthPixels;
110             int heightPixels = sweetToast.getContentView().getResources().getDisplayMetrics().heightPixels;
111             float density = sweetToast.getContentView().getResources().getDisplayMetrics().density;
112             if(widthPixels >= 1070){
113                 //例如小米5S:1920 x 1080
114                 shapeDrawable.setPadding((int)(density*13),(int)(density*12),(int)(density*13),(int)(density*12));
115             }else {
116                 //例如红米2:1280x720
117                 shapeDrawable.setPadding((int)(density*14),(int)(density*13),(int)(density*14),(int)(density*13));
118             }
119             float radius = density*8;
120             float[] outerRadii = new float[]{radius,radius,radius,radius,radius,radius,radius,radius};
121             int width = sweetToast.getContentView().getWidth();
122             int height = sweetToast.getContentView().getHeight();
123             RectF rectF = new RectF(1,1,width-1,height-1);
124             RoundRectShape roundRectShape = new RoundRectShape(outerRadii,rectF,null);
125             shapeDrawable.setShape(roundRectShape);
126             DrawableCompat.setTint(shapeDrawable,backgroundColor);
127             return shapeDrawable;
128         }catch (Exception e){
129             Log.e("幻海流心","e:"+e.getLocalizedMessage()+":154");
130         }
131         return null;
132     }
133     /**
134      * 自定义SweetToast实例的入场出场动画
135      * @param windowAnimations
136      * @return
137      */
138     public SweetToast setWindowAnimations(@StyleRes int windowAnimations){
139         mConfiguration.getParams().windowAnimations = windowAnimations;
140         return this;
141     }
142     public SweetToast setGravity(int gravity, int xOffset, int yOffset) {
143         mConfiguration.getParams().gravity = gravity;
144         mConfiguration.getParams().x = xOffset;
145         mConfiguration.getParams().y = yOffset;
146         return this;
147     }
148     public SweetToast setMargin(float horizontalMargin, float verticalMargin) {
149         mConfiguration.getParams().horizontalMargin = horizontalMargin;
150         mConfiguration.getParams().verticalMargin = verticalMargin;
151         return this;
152     }
153     /**
154      * 向mContentView中添加View
155      *
156      * @param view
157      * @param index
158      * @return
159      */
160     public SweetToast addView(View view, int index) {
161         if(mContentView != null && mContentView instanceof ViewGroup){
162             ((ViewGroup)mContentView).addView(view,index);
163         }
164         return this;
165     }
166     /**
167      * 设置SweetToast实例中TextView的文字颜色
168      *
169      * @param messageColor
170      * @return
171      */
172     public SweetToast messageColor(@ColorInt int messageColor){
173         if(mContentView !=null && mContentView.findViewById(R.id.message) != null && mContentView.findViewById(R.id.message) instanceof TextView){
174             TextView textView = ((TextView) mContentView.findViewById(R.id.message));
175             textView.setTextColor(messageColor);
176         }
177         return this;
178     }
179     /**
180      * 设置SweetToast实例的背景颜色
181      *
182      * @param backgroundColor
183      * @return
184      */
185     public SweetToast backgroundColor(@ColorInt int backgroundColor){
186         if(mContentView!=null){
187             mContentView.setBackgroundDrawable(getBackgroundDrawable(this, backgroundColor));
188         }
189         return this;
190     }
191     /**
192      * 设置SweetToast实例的背景资源
193      *
194      * @param background
195      * @return
196      */
197     public SweetToast backgroundResource(@DrawableRes int background){
198         if(mContentView!=null){
199             mContentView.setBackgroundResource(background);
200         }
201         return this;
202     }
203     /**
204      * 设置SweetToast实例的文字颜色及背景颜色
205      *
206      * @param messageColor
207      * @param backgroundColor
208      * @return
209      */
210     public SweetToast colors(@ColorInt int messageColor, @ColorInt int backgroundColor) {
211         messageColor(messageColor);
212         backgroundColor(backgroundColor);
213         return this;
214     }
215     /**
216      * 设置SweetToast实例的文字颜色及背景资源
217      *
218      * @param messageColor
219      * @param background
220      * @return
221      */
222     public SweetToast textColorAndBackground(@ColorInt int messageColor, @DrawableRes int background) {
223         messageColor(messageColor);
224         backgroundResource(background);
225         return this;
226     }
227 
228     /**
229      * 设置SweetToast实例的宽高
230      *  很有用的功能,参考了简书上的文章:http://www.jianshu.com/p/491b17281c0a
231      * @param width     SweetToast实例的宽度,单位是pix
232      * @param height    SweetToast实例的高度,单位是pix
233      * @return
234      */
235     public SweetToast size(int width, int height){
236         if(mContentView!=null && mContentView instanceof LinearLayout){
237             mContentView.setMinimumWidth(width);
238             mContentView.setMinimumHeight(height);
239             ((LinearLayout)mContentView).setGravity(Gravity.CENTER);
240             try {
241                 TextView textView = ((TextView) mContentView.findViewById(R.id.message));
242                 LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) textView.getLayoutParams();
243                 params.width = LinearLayout.LayoutParams.MATCH_PARENT;
244                 params.height = LinearLayout.LayoutParams.MATCH_PARENT;
245                 textView.setLayoutParams(params);
246                 textView.setGravity(Gravity.CENTER);
247             }catch (Exception e){
248                 Log.e("幻海流心","e:"+e.getLocalizedMessage());
249             }
250         }
251         return this;
252     }
253 
254     /**
255      * 设置SweetToast实例的显示位置:左上
256      * @return
257      */
258     public SweetToast leftTop(){
259         return setGravity(Gravity.LEFT|Gravity.TOP,0,0);
260     }
261     /**
262      * 设置SweetToast实例的显示位置:右上
263      * @return
264      */
265     public SweetToast rightTop(){
266         return setGravity(Gravity.RIGHT|Gravity.TOP,0,0);
267     }
268     /**
269      * 设置SweetToast实例的显示位置:左下
270      * @return
271      */
272     public SweetToast leftBottom(){
273         return setGravity(Gravity.LEFT|Gravity.BOTTOM,0,0);
274     }
275     /**
276      * 设置SweetToast实例的显示位置:右下
277      * @return
278      */
279     public SweetToast rightBottom(){
280         return setGravity(Gravity.RIGHT|Gravity.BOTTOM,0,0);
281     }
282     /**
283      * 设置SweetToast实例的显示位置:上中
284      * @return
285      */
286     public SweetToast topCenter(){
287         return setGravity(Gravity.TOP|Gravity.CENTER_HORIZONTAL,0,0);
288     }
289     /**
290      * 设置SweetToast实例的显示位置:下中
291      * @return
292      */
293     public SweetToast bottomCenter(){
294         return setGravity(Gravity.BOTTOM|Gravity.CENTER_HORIZONTAL,0,0);
295     }
296     /**
297      * 设置SweetToast实例的显示位置:左中
298      * @return
299      */
300     public SweetToast leftCenter(){
301         return setGravity(Gravity.LEFT|Gravity.CENTER_VERTICAL,0,0);
302     }
303     /**
304      * 设置SweetToast实例的显示位置:右中
305      * @return
306      */
307     public SweetToast rightCenter(){
308         return setGravity(Gravity.RIGHT|Gravity.CENTER_VERTICAL,0,0);
309     }
310     /**
311      * 设置SweetToast实例的显示位置:正中
312      * @return
313      */
314     public SweetToast center(){
315         return setGravity(Gravity.CENTER,0,0);
316     }
317     /**
318      * 将SweetToast实例显示在指定View的顶部
319      * @param targetView    指定View
320      * @param statusHeight  状态栏显示情况下,状态栏的高度
321      * @return
322      */
323     public SweetToast layoutAbove(View targetView, int statusHeight){
324         if(mContentView!=null){
325             int[] locations = new int[2];
326             targetView.getLocationOnScreen(locations);
327             //必须保证指定View的顶部可见
328             int screenHeight = ScreenUtil.getScreenHeight(mContentView.getContext());
329             if(locations[1] > statusHeight&&locations[1]<screenHeight){
330                 setGravity(Gravity.BOTTOM|Gravity.CENTER_HORIZONTAL,0,screenHeight - locations[1]);
331             }
332         }
333         return this;
334     }
335     /**
336      * 将SweetToast实例显示在指定View的底部
337      * @param targetView
338      * @param statusHeight
339      * @return
340      */
341     public SweetToast layoutBellow(View targetView, int statusHeight){
342         if(mContentView!=null){
343             int[] locations = new int[2];
344             targetView.getLocationOnScreen(locations);
345             //必须保证指定View的底部可见
346             int screenHeight = ScreenUtil.getScreenHeight(mContentView.getContext());
347             if(locations[1]+targetView.getHeight() > statusHeight&&locations[1]+targetView.getHeight()<screenHeight){
348                 setGravity(Gravity.TOP|Gravity.CENTER_HORIZONTAL,0,locations[1]+targetView.getHeight()-statusHeight);
349             }
350         }
351         return this;
352     }
353 
354 
355     /**********************************************  SweetToast显示及移除  **********************************************/
356     Handler mHandler = new Handler();
357     Runnable mHide = new Runnable() {
358         @Override
359         public void run() {
360             handleHide();
361         }
362     };
363     protected void handleHide() {
364         if(this != null && mContentView != null){
365             if(stateChangeEnabled){
366                 if(hideEnabled){
367                     if(showing){
368                         mWindowManager.removeView(mContentView);
369                     }
370                     showing = false;
371                     mContentView = null;
372                 }else{
373                 }
374             }
375         }
376     }
377     protected void handleShow() {
378         if(mContentView != null){
379             if(stateChangeEnabled){
380                 if(showEnabled){
381                     try {
382                         mWindowManager.addView(mContentView,mConfiguration.getParams());
383                         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());
384                         mHandler.postDelayed(mHide,delay);
385                         showing = true;
386                     }catch (Exception e){
387                         Log.e("幻海流心","e:"+e.getLocalizedMessage()+":213");
388                     }
389                 }
390             }
391         }
392     }
393 
394     /**
395      * 保持当前实例的显示状态:不允许向Window中添加或者移除View
396      */
397     protected void removeCallbacks(){
398         stateChangeEnabled = false;
399     }
400 
401     /**
402      * 设置是否允许展示当前实例
403      * @param showEnabled
404      */
405     public void setShowEnabled(boolean showEnabled) {
406         this.showEnabled = showEnabled;
407     }
408 
409     /**
410      * 设置是否允许移除当前实例中的View
411      * @param hideEnabled
412      */
413     public void setHideEnabled(boolean hideEnabled) {
414         this.hideEnabled = hideEnabled;
415     }
416 
417     /**
418      * 设置是否允许改变当前实例的展示状态
419      * @param stateChangeEnabled
420      */
421     public void setStateChangeEnabled(boolean stateChangeEnabled) {
422         this.stateChangeEnabled = stateChangeEnabled;
423     }
424 
425     /**
426      * 将当前实例添加到队列{@link SweetToastManager#queue}中,若队列为空,则加入队列后直接进行展示
427      */
428     public void show(){
429         try {
430             if (Build.VERSION.SDK_INT >= 23) {
431                 //Android6.0以上,需要动态声明权限
432                 if(mContentView!=null && !Settings.canDrawOverlays(mContentView.getContext().getApplicationContext())) {
433                     //用户还未允许该权限
434                     Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
435                     mContentView.getContext().startActivity(intent);
436                     return;
437                 } else if(mContentView!=null) {
438                     //用户已经允许该权限
439                     SweetToastManager.show(this);
440                 }
441             } else {
442                 //Android6.0以下,不用动态声明权限
443                 if (mContentView!=null) {
444                     SweetToastManager.show(this);
445                 }
446             }
447 //            SweetToastManager.show(this);
448         }catch (Exception e){
449             Log.e("幻海流心","e:"+e.getLocalizedMessage()+":232");
450         }
451     }
452     /**
453      * 利用队列{@link SweetToastManager#queue}中正在展示的SweetToast实例,继续展示当前实例的内容
454      */
455     public void showByPrevious(){
456         try {
457             if (Build.VERSION.SDK_INT >= 23) {
458                 //Android6.0以上,需要动态声明权限
459                 if(mContentView!=null && !Settings.canDrawOverlays(mContentView.getContext().getApplicationContext())) {
460                     //用户还未允许该权限
461                     Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
462                     mContentView.getContext().startActivity(intent);
463                     return;
464                 } else if(mContentView!=null) {
465                     //用户已经允许该权限
466                     SweetToastManager.showByPrevious(this);
467                 }
468             } else {
469                 //Android6.0以下,不用动态声明权限
470                 if (mContentView!=null) {
471                     SweetToastManager.showByPrevious(this);
472                 }
473             }
474 //            SweetToastManager.showByPrevious(this);
475         }catch (Exception e){
476             Log.e("幻海流心","e:"+e.getLocalizedMessage()+":290");
477         }
478     }
479     /**
480      * 清空队列{@link SweetToastManager#queue}中已经存在的SweetToast实例,直接展示当前实例的内容
481      */
482     public void showImmediate(){
483         try {
484             if (Build.VERSION.SDK_INT >= 23) {
485                 //Android6.0以上,需要动态声明权限
486                 if(mContentView!=null && !Settings.canDrawOverlays(mContentView.getContext().getApplicationContext())) {
487                     //用户还未允许该权限
488                     Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
489                     mContentView.getContext().startActivity(intent);
490                     return;
491                 } else if(mContentView!=null) {
492                     //用户已经允许该权限
493                     SweetToastManager.showImmediate(this);
494                 }
495             } else {
496                 //Android6.0以下,不用动态声明权限
497                 if (mContentView!=null) {
498                     SweetToastManager.showImmediate(this);
499                 }
500             }
501 //            SweetToastManager.showImmediate(this);
502         }catch (Exception e){
503             Log.e("幻海流心","e:"+e.getLocalizedMessage()+":252");
504         }
505     }
506     /**
507      * 移除当前SweetToast并将mContentView置空
508      */
509     public void hide() {
510         mHandler.post(mHide);
511     }
512     /**********************************************  SweetToast显示及移除  **********************************************/
513 
514     //Setter&Getter
515     public View getContentView() {
516         return mContentView;
517     }
518     public void setContentView(View mContentView) {
519         this.mContentView = mContentView;
520     }
521     public SweetToastConfiguration getConfiguration() {
522         return mConfiguration;
523     }
524     public void setConfiguration(SweetToastConfiguration mConfiguration) {
525         this.mConfiguration = mConfiguration;
526     }
527     public WindowManager getWindowManager() {
528         return mWindowManager;
529     }
530     public void setWindowManager(WindowManager mWindowManager) {
531         this.mWindowManager = mWindowManager;
532     }
533     public boolean isShowing() {
534         return showing;
535     }
536     public void setShowing(boolean showing) {
537         this.showing = showing;
538     }
539 }

源码及所在DEMO已上传至GitHub:SweetTips,欢迎大家提Bug,喜欢的话记得Star或Fork下哈!

That's all !

posted @ 2017-01-12 14:58  幻海流心  阅读(417)  评论(0编辑  收藏  举报