此文章是我在简书的文章,自行搬到博客园.简书地址:SweetTips: 快意灵动的Android提示库!
源码及所在DEMO已上传至GitHub:SweetTips,欢迎大家提Bug,喜欢的话记得Star或Fork下哈!
1.为什么要写这个库?
上面的问题也可以这样问:有哪些常见的需求,Android原生Toast及Design包中的Snackbar实现起来相对繁琐?Toast:
- 原生Toast无法/不方便自定义显示时间;
- 原生Toast,需要等待队列中前面的Toast实例显示完毕之后才可以显示,实时性差;
- 原生Toast,想在正在显示的Toast实例上显示新的内容并设置新内容的显示时间,实现较繁琐;
- 原生Toast,无法/不方便自定义动画;
- 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 !