查看原文
其他

一款对Toast,Snackbar,Dialog进行优化与兼容封装的开源库

Coder_朱志强 郭霖 2019-04-29



今日科技快讯


哈啰出行APP将于2月22日在全国范围内上线顺风车业务。除官方APP外,在哈啰出行的支付宝小程序上,用户也可以直接打到顺风车。其实从1月25日起,哈啰出行已陆续在广州、杭州、成都、北京、武汉等22座城市上线顺风车服务,并拿出3000万元共享春运基金作为补贴。


作者简介


本篇来自 Coder_朱志强 的投稿,分享了一些常用提示控件的封装,希望对大家有所帮助!

Coder_朱志强 的博客地址

https://blog.csdn.net/jungle_pig


SmartToast


BadTokenException解决方案

Android 7.1系统上,Toast会偶现BadTokenException。理解产生的原因,需要对Toast的基本工作原理以及不同系统版本Toast源码的变化有所了解,完整讲述可参考我的Toast系列博文(https://blog.csdn.net/jungle_pig/article/details/81504199)

说明:引用到系统源码时,我自己加的注释会标明“笔者注释”。为便于读者理解主要流程,无关代码用省略号代替。

限于篇幅,我简要说一下。Android 7.1开始,Google开始限制TYPE_TOAST窗口的滥用,系统在将Toast请求加入队列时,会为其创建一个Token。

@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration){
   ...
   Binder token = new Binder();
   //为该Toast窗口添加Token,笔者注释
   mWindowManagerInternal.addWindowToken(token,WindowManager.LayoutParams.TYPE_TOAST);
   record = new ToastRecord(callingPid, pkg, callback, duration, token);
   //将该Toast请求加入队列,笔者注释
   mToastQueue.add(record);
   ...
}

当Toast超时(duration耗尽)或者调用了cancel方法需要隐藏时,系统将这个显示请求从队列移除,并将Token设为失效.

    void cancelToastLocked(int index) {
       //从队列中把本次请求取出,笔者注释
       ToastRecord record = mToastQueue.get(index);
        ...
        //调用callback的hide方法隐藏掉窗口,这个callback实际上就是Toast的Tn,笔者注释
        record.callback.hide();
        ...
        //将该Toast请求从队列移除,笔者注释
        ToastRecord lastToast = mToastQueue.remove(index);
        //将该Toast窗口的Token设为失效,笔者注释
        mWindowManagerInternal.removeWindowToken(lastToast.token, true);
        ...
        //如果队列不为空,则说明还有Toast要显示,则继续显示下一个Toast,笔者注释
        if (mToastQueue.size() > 0) {
            showNextToastLocked();
        }
    }

Toast的工作流程是一个基于Binder的IPC(进程通信)过程,应用程序作为客户端仅仅发起Toast请求和被动接受回调。系统服务负责管理请求队列、Token等,并通过Toast的内部类Tn来实现与应用程序的交互,即通过回调Tn的show/hide方法来显示/隐藏Toast窗口。

应用程序的主线程是一个死循环,不断地从消息队列里取出消息执行。系统服务回调Tn的show方法,实际上的执行逻辑是发送一个SHOW消息给Tn的Handler,并最终在这个Handler的handleShow方法里执行具体的显示逻辑。

Tn的show方法:

@Override
public void show(IBinder windowToken) {
    ...
    mHandler.obtainMessage(0, windowToken).sendToTarget();
}

Handler的handleShow方法:

public void handleShow(IBinder windowToken) {
    ...
    //将系统服务创建的Token传递进来,笔者注释
    mParams.token = windowToken;
    ...
    //添加Toast窗口,若此时Token已然失效,引发BadTokenExceptin,笔者注释
    mWM.addView(mView, mParams);
    ...
}

如果系统服务已然调用了Tn的show方法,而恰在此时,应用程序主线程因为某个消息阻塞或者其他原因,迟迟没能执行到handleShow方法。直到Toast超时,系统服务将该Toast请求从队列移除,并将Token设为失效。在这之后,应用程序才执行到handleShow方法,而Token已然失效,添加Toast窗口必然发生BadTokenException。

Google在Android8.0修复了这个bug。

首先,直接在产生BadTokenException的地方捕获该异常。

public void handleShow(IBinder windowToken) {
        ...
        //捕获该异常,笔者注释
        try {
            mWM.addView(mView, mParams);
        ...
        } catch (WindowManager.BadTokenException e) {
            /* ignore */
        }
    }
}

另外,在Tn的handler处理SHOW消息之前,判断其消息队列里是否存在HIDE或者CANCEL消息,有则表示Token已然失效,直接返回,什么都不需要做。

public void handleShow(IBinder windowToken) {
    ...

    //判断是否有超时隐藏或者主动cancel的消息,有则表示系统服务已经将其从
    //Toast队列移除,并将Token设为失效,笔者注释
    if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
        return;
    }

    ...

        try {
            mWM.addView(mView, mParams);
            ...
        } catch (WindowManager.BadTokenException e) {
            /* ignore */
        }

}

但是,对于已经发布了的Android7.1,我们如何解决这个问题呢?我们可以仿照8.0的补救方法,直接添加try-catch块捕获它。注意,不能想当然的像下面这样捕获。

try {
    toast.show();
}catch (WindowManager.BadTokenException e){

}

Toast的show方法仅仅是创建Tn提交给系统服务,请求显示Toast而已。前面说过,BadTokenException发生在handleShow方法内,而handleShow方法是在Tn的Handler的handleMessage方法内被调用的。我们可以新建一个Handler,命名为SafeHandler,作为Tn原有Handler的外壳。SafeHandler的handleMessage方法直接转交给原有Handler处理,只是在外层套上try-catch块。最后将SafeHandler注入Tn,取代原有Handler。

SafeHandler源码:

class SafeHandler extends Handler {
    //用来保存Tn原有handler
    private Handler mNestedHandler;

    public SafeHandler(Handler nestedHandler) {
        //构造方法里将Tn原有Handler传入
        mNestedHandler = nestedHandler;
    }

    /**
     * 在dispatchMessage里会调用handleMessage方法,可以直接在这里捕获异常
     * @param msg
     */

    @Override
    public void dispatchMessage(Message msg) {
        try {
            super.dispatchMessage(msg);
        } catch (WindowManager.BadTokenException e) {
        }
    }

    @Override
    public void handleMessage(Message msg) {
        //交由原有Handler处理
        mNestedHandler.handleMessage(msg);
}

通过反射拿到Tn,将SafeHandler注入

 Field tnField = Toast.class.getDeclaredField("mTN");
  tnField.setAccessible(true);
  mTn = tnField.get(mToast);

  if (isSdk25()) {
      Field handlerField = mTn.getClass().getDeclaredField("mHandler");
      handlerField.setAccessible(true);
      Handler handlerOfTn = (Handler) handlerField.get(mTn);
      handlerField.set(mTn, new SafeHandler(handlerOfTn));
 }

类型消息

github上有不少花样Toast,五彩斑斓。SmartToast并不打算效仿,主流App很少采用如此花哨的消息提示。主流App如淘宝、微信、优酷、微博等等的消息提示,与ios非常相似。一是android UI确实丑,二是UI设计人员大多基于ios设计,很自然地把ios的风格带进android。SmartToast提供了8种常见的消息提示。

//普通
SmartToast.info("已在后台下载");
SmartToast.infoLong("已在后台下载");
//成功    
SmartToast.success("重置成功");
SmartToast.successLong("重置成功");
//错误
SmartToast.error("保存失败");
SmartToast.errorLong("保存失败");
//警告
SmartToast.warning("电量过低,请充电");      
SmartToast.warningLong("电量过低,请充电");
//完成
SmartToast.complete("下载完成");
SmartToast.completeLong("下载完成");
//失败
SmartToast.fail("保存失败");
SmartToast.failLong("保存失败");
//禁止
SmartToast.forbid("当前账户不允许汇款操作");
SmartToast.forbidLong("当前账户不允许汇款操作");
//等候
SmartToast.waiting("已在后台下载,请耐心等待");
SmartToast.waitingLong("已在后台下载,请耐心等待");

新的复用策略

在1.x版本中说过,Toast存在两个问题,一是当弹出一个新的Toast时,需等到前一个Toast的duration耗尽才弹出;二是短时间内多次触发相同内容的Toast会重复弹出。当然,大多厂商设备在android 7.0左右往后,都做了不同程度的优化,可能部分或全部避免了上述的问题。最常见的优化方式大致如下:

public final class ToastUtil {
    private ToastUtil() {

    }

    //toast 单例
    private static Toast sToast;
    //默认的xOffset
    private static int sDefaultXOffset;
    //默认的yOffset
    private static int sDefaultYOffset;


    private static void createToast(CharSequence msg, int duration) {
        if (sToast == null) {
            //创建Toast单例,并保存默认的xOffset和yOffset
            sToast = Toast.makeText(MyApplication.sContext, "", Toast.LENGTH_SHORT);
            sDefaultXOffset = sToast.getXOffset();
            sDefaultYOffset = sToast.getYOffset();
        }
        //修改message、duration
        sToast.setText(msg);
        sToast.setDuration(duration);
    }

    //默认位置显示Toast
    public static void showToast(CharSequence msg, int duration) {
        createToast(msg, duration);
        sToast.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, sDefaultXOffset, sDefaultYOffset);
        sToast.show();
    }

    //居中显示Toast
    public static void showCenterToast(CharSequence msg, int duration) {
        createToast(msg, duration);
        sToast.setGravity(Gravity.CENTER, 00);
        sToast.show();
    }
}

简单来说,就是复用Toast单例,每次显示时,修改message、duration、gravity等参数后调用Toast的show方法。

如此会存在两个缺陷。①如果本次显示尚未结束,再次显示不同文本的Toast无弹出效果;②如果本次显示尚未结束,再次显示不同位置(gravity)的Toast,Toast的位置并不会发生变化。前者尚可接受,后者就不可原谅了。看一下效果:

我们可以看到,第一遍,每次显示都等上一次显示结束后进行,结果是正常的。第二遍,反之,发现,没有动画,位置也没有改变。这是为什么呢?

原因是这样的:对于同一个Toast实例,多次调用show方法发起显示请求,如果它已在显示队列里,系统服务只会更改其duration,并没有添加新窗口。

下面两段代码取自sdk25。

 @Override
 public void enqueueToast(String pkg, ITransientNotification callback, int duration){
        ...
        //ToastRecord 代表一次Toast显示请求,笔者注释
        ToastRecord record;
        //根据callback查找ToastRecord,callback其实就是Tn,Toast实例和Tn是一对一关系,笔者注释
        int index = indexOfToastLocked(pkg, callback);
        // 如果存在,则只更新duration,笔者注释
        if (index >= 0) {
           record = mToastQueue.get(index);
           record.update(duration);
           } else {
              ...
                  }
         ...                
  }


private static final class ToastRecord{
   ...
        void update(int duration) {
            this.duration = duration;
        }
    ...
}

弹出动画和gravity都是对窗口起作用的,既然没有添加新的窗口,那么没有效果是正常的。而文本能够改变只是窗口内TextView自身重绘而已。

而且,自Android8.0开始,google及各大厂商都对Toast进行了优化。这种单例使用模式在部分设备上存在严重问题。我是在各大厂商的开发者平台进行远程真机测试时发现的。下面以华为麦芒7设备(android 8.1.0)为例,同一个Toast实例,短时间多次调用show方法,Toast会立即消失,而且也不会弹出新的Toast,持续触发,Toast将一直不显示。看下效果:

同样的,问题也是出现在本次显示尚未结束,再次调用show方法显示的时候。

曾经,在1.x版本中,我们通过反射调用Tn的hide方法,解决gravity不生效及消息文本改变无动画的问题,在Android8.0及以上设备上并不理想。

综合以上原因,为了尽可能兼容所有设备,我改变了Toast的复用策略。

首先,当一个Toast触发后,从显示到消失的时间内,再次触发相同的Toast(内容及位置一致),直接忽略。也就是说不再重复调用Toast的show方法。

另外,触发一个与上次显示内容或位置不相同的Toast,并且当前Toast尚未消失,不再调用Tn隐藏,而是直接cancel掉,并重建Toast实例。这种情况发生的次数较少(这也是很多人没有注意到这种场合下Gravity不能立即生效的原因),所以并不会引起频繁创建Toast实例,绝大多数时候都是复用实例。

现在看一下,上面两个案例,用SmartToast来显示的效果:

①魅族 pro 6 plus(android 7.0)

②华为麦芒 7(android 8.1.0)

小的优化

①改变Toast的背景不能通过setBackgroundColor。因为Toast的背景实际上是一张图片,而且不同厂商设备使用的图片大小和圆角不尽相同。如魅族的圆角就比较小,而华为的就比较大。如果直接设置setBackgroundColor,你的Toast就成方方正正的了,哈哈。在1.x版本中,我们采用ShapeDrawable设置,忽略不同厂商设备差异,设置了固定的圆角。在2.x我们改变了策略,直接获取Toast的背景,得到Drawable图片,如果是GradientDrawable实例,则直接设置新的颜色,否则一律采用Tint机制,改变其颜色。

@Override
protected void setupToast() {
   ...
   Drawable bg = mView.getBackground();

   if (bg instanceof GradientDrawable) {
     ((GradientDrawable) bg).setColor(ToastDelegate.get().getToastSetting().getBgColor());
      } else {
     DrawableCompat.setTint(bg, ToastDelegate.get().getToastSetting().getBgColor());
      }

   mView.setBackgroundDrawable(bg);
   ...
}

这样定制的Toast的大小和形状以及透明度都与目标设备完全一致了,仅仅颜色不同。

②离开当前Activity,Toast自动消失

Toast是独立窗口,并不会随着Activity的销毁而消失。在1.x版本发布时,有读者询问能否设置离开当前页面后Toast立即消失。2.x版中,只需如此设置:

SmartToast.setting()
    .dismissOnLeave(true);

这样,无论是进入新的activity还是退出当前Activity,当前显示的Toast都会立即消失。


SmartTopbar


这种Topbar类似QQ、微信顶部弹窗。在代码实现上,可以说我是夺天之功。灵感是这么来的,在1.x版本中,有读者问是否能够实现顶部弹出的Snackbar。Snackbar是相当优秀的底部弹窗了,如果通过修改Snackbar实现类似QQ顶部弹窗功能是再好不过了。不过QQ、微信弹窗是独立窗口,可以悬浮于应用之外,Snackbar是依附于Activity的,只能应用内弹出。大多数应用的使用场景都是应用内弹窗,而且独立弹窗需要危险权限,用户拒绝则无法显示,且各大厂商设备关于悬浮窗方面的坑又比比皆是,所以应用内顶部弹窗不采用独立悬浮窗会更好一些。

开始改造过程。

首先,改变显示位置。1.x版本中说过,Snackbar是通过将View(实现类SnackbarLayout)嵌入到当前Activity的id为android.R.id.content的容器内或者某个CoordinateLayout中,具体会根据你提供的view为根基,沿着整个View Tree上溯,先找个哪个,就将其嵌入其内,且layout_gravity为bottom。

<view xmlns:android="http://schemas.android.com/apk/res/android"
      class="android.support.design.widget.Snackbar$SnackbarLayout"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_gravity="bottom"
      android:theme="@style/ThemeOverlay.AppCompat.Dark"
      style="@style/Widget.Design.Snackbar" />

是不是直接把layout_gravity改为top就完事了?当然还不够,因为无论哪种嵌入方式,snackbar最终都是android.R.id.content这个容器的子View,而android.R.id.content 容器是位于状态栏下面的,弹窗是无法覆盖住状态栏的,状态栏的颜色和弹窗不一致的话,就不太协调了。当然,如果不考虑提供给别人使用的话,可以通过setSystemUiVisibility使其浸入状态栏。为了通用,我们将其嵌入DecorView。

Snackbar 是通过findSuitableParent(View view)方法来确定嵌入哪个容器的,我们修改它的实现,不再按照旧的逻辑沿View Tree上溯,而是直接返回传入的View,然后我们创建Topbar的时候直接将DecorView传入。

private static ViewGroup findSuitableParent(View view) {
   return (ViewGroup) view;
}

这样,Topbar就会盖住状态栏了。

QQ、微信的顶部弹窗都是可以手滑消失的,Snackbar只有以CoordinateLayout为容器的时候才支持手滑消失。我们需要自己实现手滑消失么?No,牛顿哥说过,站在巨人的肩膀上,我们才能走的更远。哈哈。继续夺天之功。在嵌入Topbar之前,我先嵌入一个CoordinateLayout到DecorView,然后再将Topbar嵌入CoordinateLayout中,Topbar就可以支持手滑了。

/**
 * 获取Topbar的入口
 * @param activity
 * @return
 */

public static IBarShow get(Activity activity) {
    return TopbarDelegate.get().nestedDecorView(activity);
}

通过预定义的id值smart_show_top_bar_container,判断CoordinateLayout是否已嵌入,若不存在则先嵌入CoordinateLayout

public IBarShow nestedDecorView(Activity activity) {
        //保存当前页面的Context
        mPageContext = activity;
        //取出DecorView
        ViewGroup decorView = activity == null ? null : (ViewGroup) activity.getWindow().getDecorView();
        CoordinatorLayout topbarContainer = null;
        if (decorView != null) {
            //判断CoordinateLayout是否已嵌入,不存在则先创建CoordinateLayout
            topbarContainer = decorView.findViewById(R.id.smart_show_top_bar_container);
            if (topbarContainer == null) {
                topbarContainer = new CoordinatorLayout(activity);
                topbarContainer.setId(R.id.smart_show_top_bar_container);
                ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT
                );
                //将CoordinateLayout嵌入DecorView
                decorView.addView(topbarContainer, lp);
            }

        }
        return getFromView(topbarContainer);
}

在上面,CoordinateLayout的布局参数中,宽度设置为WRAP_CONTENT。Topbar消失时,布局从CoordinateLayout中移除,高度为0,CoordinateLayout不会绘制,不影响原有布局。CoordinateLayout没有设置layout_gravity为bottom,所以会顶部显示,SnackbarLayout被CoordinateLayout包裹着,也就不必修改layout_gravity为top了。

以CoordinateLayout为容器,将Topbar嵌入

protected IBarShow getFromView(View view) {
    if (mBar == null || mBaseTraceView != view || isDismissByGesture()) {
        mBaseTraceView = view;
        rebuildBar(view);
    }
    return this;
}

最后,就剩下动画了。显示时向下弹出,消失时向上弹去。

Snackbar显示和消失动画最终是在animateViewIn()和animateViewOut(final int event)方法里设置的,实现上区分了sdk12(含)以上和以下的情况。我们的库最低支持sdk15,所以只考虑sdk >= 12的情况。

先分析Snackbar的显示动画,源码基于design 27.0.1

void animateViewIn() {
        ...
        //取出View的高度,通过offset或者translationY的方式使其处于初始显示位置,
        //也就是所嵌容器的下面,一般为屏幕下面,笔者注释
        final int viewHeight = mView.getHeight();
        if (USE_OFFSET_API) {
            ViewCompat.offsetTopAndBottom(mView, viewHeight);
        } else {
            mView.setTranslationY(viewHeight);
        }

        //定义属性动画,变化值从view高度值到0,
        final ValueAnimator animator = new ValueAnimator();
        animator.setIntValues(viewHeight, 0);
        animator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
        animator.setDuration(ANIMATION_DURATION);
        ...
        //注册动画值变化监听器,按照变化值动态设置offset或者translationY形成动画,笔者注释
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            private int mPreviousAnimatedIntValue = viewHeight;

            @Override
            public void onAnimationUpdate(ValueAnimator animator) {
                int currentAnimatedIntValue = (int) animator.getAnimatedValue();
                if (USE_OFFSET_API) {
                    ViewCompat.offsetTopAndBottom(mView,
                            currentAnimatedIntValue - mPreviousAnimatedIntValue);
                } else {
                    mView.setTranslationY(currentAnimatedIntValue);
                }
                mPreviousAnimatedIntValue = currentAnimatedIntValue;
            }
        });
        animator.start();
    }
}

总结一下,先取出View的高度,通过offset或者translationY的方式使其处于初始显示位置,即所嵌容器的下面,一般也就是屏幕下面。然后定义属性动画,变化值从view高度值到0,最后注册动画值变化监听器,按照变化值动态设置offset或者translationY形成动画。animateViewOut方法与此类似,只是定义的变化值是0到View高度,这里不再贴出。

要想达到显示时向下弹出,消失时向上弹去,只需将变化值分为别设为负的view高度到0和0到负的View高度即可。

新定义一个方法,获取设置动画时,提供的view height。

private int getAnimHeight() {
    return -mView.getHeight();
}

分别在animateViewIn和animateViewOut中替换mView.getHeight(),以animateViewIn为例

    void animateViewIn() {
        //替换mView.getHeight()
        final int viewHeight = getAnimHeight();
        if (USE_OFFSET_API) {
            ViewCompat.offsetTopAndBottom(mView, viewHeight);
        } else {
            mView.setTranslationY(viewHeight);
        }
        final ValueAnimator animator = new ValueAnimator();
        animator.setIntValues(viewHeight, 0);
        ...
    }

复用策略

Topbar由Snackbar改造而来,所以复用策略是一样的,详情参看github文档。并且不同于1.x版中需要你在BaseActivity中调用资源回收的方法,2.x版本通过为application注册activityLifeCallback,自动回收资源及重建实例。

sApplication.registerActivityLifecycleCallbacks(new ActivityLifecycleCallback() {
    ...

    @Override
    public void onActivityDestroyed(Activity activity) {
        super.onActivityDestroyed(activity);
        if (SnackbarDeligate.hasCreated()) {
            SnackbarDeligate.get().destroy(activity);
        }

        if (TopbarDelegate.hasCreated()) {
            TopbarDelegate.get().destroy(activity);
        }
    }

});

如此,我们便巧夺天工般实现了顶部弹窗,你完全可以把它当做顶部Snackbar使用,它的api和SmartSnackbar几乎完全一致。


SmartDialog


Dialog的使用过程中有可能出现下面三个问题:

①NullPointerException

这个问题是因为创建Dialog时传入的activity为null。一种常见的场景是在Fragment中创建Dialog时直接传入getActivity()。等到Fragment与activity解除关系时(detach),getActivity为null。此时创建Dialog引发NullPointerException。

②BadTokenException

显示对话框时,所依附的activity已经销毁了,导致BadTokenException。

③IllegalStateException

调用dimiss时,所依附的activity已经销毁了,引发IllegalStateException(DecorView not attached to window manager)。我的设备上没有复现,是在友盟崩溃上捕获到的。

其实上面的三种情况归根结底都是由生命周期引起的。举一个例子,网络访问的回调中显示Dialog,如果在接口返回之前当前Activity已退出,则引发BadTokenException。当然通常接口回调时,要根据activity生命周期决定是否取消掉操作UI的回调。不过,引发场景不只这一个,作为View层,我们要把好最后一道关。

SmartDialog在创建、显示Dialog及调用dismiss前,会判断activity的生命周期,生命周期不符合则不创建、不显示、不操作。

API

第一步,继承DialogCreator,实现你的Dialog创建逻辑。

public class ExampleDialogCreator extends DialogCreator {
    /**
     * 抽象方法,必须实现
     *
     * @param activity
     * @return
     */

    @Override
    public Dialog createDialog(Activity activity) {
        //创建Dialog,在这里可以保证activity不为null,并且没有destroyed或isFinishing
        Dialog dialog = null;
        ...
        return dialog;
    }

    /**
     * 非抽象方法,默认实现为空,可选择性覆写,用于Dialog每次显示前的一些重置工作,例如EditText清空等
     *
     * @param dialog
     */

    @Override
    public void resetDialogPerShow(Dialog dialog) {
        super.resetDialogPerShow(dialog);

    }
}

第二步,创建SmartDialog,传入DialogCreator。实际上SmartDialog并没有继承Dialog,只是个Dialog的包装器,管理Dialog。reuse(boolean b)方法表示是否复用Dialog,如果不复用,每次显示都调用DialogCreator创建新的Dialog。

private SmartDialog mExampleDialog;

public void onShowDialogClick(View view) {
    if (mExampleDialog == null) {
        mExampleDialog = new SmartDialog()
                .dialogCreator(new ExampleDialogCreator())
                .reuse(true);
    }
    mExampleDialog.show(this);
}

SmartDialog的show方法和dismiss方法都需要传入activity,对activity的生命周期进行判断,避免BadTokenException等异常。

/**
 * 判断 activity的状态是可以操作UI
 * @param activity
 * @return
 */

public static boolean isUpdateActivityUIPermitted(Activity activity) {
    return activity != null || !activity.isFinishing() || !Utils.isActivityDestroyed(activity);
}

为了双重保险,对可能抛出异常的地方用try-catch包裹。

//mNestedDialog是被SmartDialog管理的Dialog实例
if (mNestedDialog != null) {
    try {
        mNestedDialog.show();
        return true;
    } catch (WindowManager.BadTokenException e) {
        EasyLogger.e("BadToken has happened when show dialog: \n"
        + mNestedDialog.getClass().getSimpleName());
        return false;
    }
}

预定义的DialogCreator

预定了INotificationDialogCreator、IEnsureDialogCreator、IInputTextDialogCreator、ILoadingDialogCreator四种DialogCreator。通过DialogCreatorFactory获取相关实例。

private SmartDialog mResetSuccTip;

public void onShowTip() {
    if (mResetSuccTip == null) {
        mResetSuccTip = new SmartDialog()
                .dialogCreator(DialogCreatorFactory.notification().message("重置成功"))
                .reuse(true);
    }

    mResetSuccTip.show(this);
}

预定义的DialogCreator可调用方法设置按钮文本和颜色、按钮点击事件、标题等等。具体可参见github上的文档。

下面是各种预定义DialogCreator创建的Dialog的展示图:

最后,再次感谢大家的支持,项目地址:

https://github.com/the-pig-of-jungle/smart-show


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存