【Android】当关闭通知权限后无法显示Toast的解决方案V2.0

来源:互联网 发布:mac osx yosemite.vdi 编辑:程序博客网 时间:2024/05/18 01:24

前言

既然能看到这篇博文,就说明你一定看过这个【Android】当关闭通知消息权限后无法显示系统Toast的解决方案 。然后,我很开心的告诉你,兄弟,可能你们的心病这次就解决了~当然,没看过上一篇博文的还是建议看下,看看以前的解决思路,看看之前的实现方式和一些遇到的问题,说不定对你也很有收获呢。

怎么使用

github地址:https://github.com/Blincheng/EToast2
如果大家有任何什么问题,欢迎大家加入EToast交流群和我一起讨论和改进。
QQ群:EToast交流群
群号码:547279762
这里写图片描述
(PS:目前v2.1.0已经正式上线,经过一个多月的测试和改进,此版本相对接近完美,欢迎大家使用,
具体改进过程请移步github)

Step 1. Add the JitPack repository to your build file

allprojects {    repositories {        ...        maven { url 'https://jitpack.io' }    }}

Step 2. Add the dependency

dependencies {    compile 'com.github.Blincheng:EToast2:v2.1.0'}

然后,就大大咧咧的用吧

EToast 一个关闭系统消息通知后依然可以显示的Toast,此版本完全是独立于v1.x.x的版本,实现方式上也是完全的不同,尽量的参考系统Toast的源码去实现。
和上代EToast相比,有以下的改动:
1. Context不再依赖于Activity显示。
2. 显示动画完全跟随着系统走,也就是说和系统的Toast动画效果完全一致
3. 多条显示规则还是保留了V1.x的版本的规则,永远只显示一个Toast。
4. 由于实现原理的更改,EToast不再会被Dialog、PopupWindow等弹窗布局覆盖

由于在Android5.0以下无法获取是否打开系统通知权限,所以为了防止用户看不到Toast,最终的逻辑优化了一下:
1. 当用户是5.0以下的机器时,永远只显示EToast
2. 当用户是5.0以上的机器是,如果打开了通知权限,则显示系统Toast;反之则显示Etoast

Toast.makeText(mActivity, text, EToast2.LENGTH_SHORT).show();

需要注意的是,此Toast非彼Toast,应该使用“import com.mic.etoast2.Toast”,建议在BaseActivity中如下使用:

public void showShortText(String text) {    Toast.makeText(mActivity, text, Toast.LENGTH_SHORT).show();}

好了,没有兴趣想看看怎么实现的,只是来拿货的,你就可以走了/(ㄒoㄒ)/~~

然后本文会有点长~请耐心往下看,哈哈哈。

实现思路

首先思考下这个版本要做什么事

  1. 首先,Context的使用限制,之前只能是Activity,这个有点忧伤,相当于直接依赖Activity,使用上的确有点不踏实哈~
  2. 然后,不能在顶层显示,会被键盘、dialog等遮挡,如果有这种场景的话,比较尴尬。
  3. 内存泄漏,由于引用了当前Activity的上下文
  4. 显示动画是不是应该跟着系统走,好让Toast和其他App一致。(反正目的就是说不管怎么样,一定让Toast显示呗)
  5. 貌似管理机制不是很好,能不能和Activity或者Fragment的生命周期保持一致呢?

那么,怎么解决

刚开始我也脑子中思考了许久,想想有什么好的办法可以解决以上问题呢?当然,结果肯定是有的。不过貌似也遇到了一些坎坷。刚好呢,我之前抽空看了一下Glide的源码,因为类似的图片框架加载就和Activity、Fragment等的声明周期密切相关~具体Glide的源码我就不展开了,简单说下:

Glide.with(this).load(url).into(imageView);

上面就是Glide的最简单用法,whit(this)在一开始就把当前环境丢进去了哈~然后那么久开始仿照直接写呗。(忘记说了哈~由于之前好像说过以后的博文直接用Kotlin开发,不知道大家能不能习惯哈~)

companion object{        var style = android.R.style.Theme_Light_NoTitleBar        val LENGTH_SHORT = 0        val LENGTH_LONG = 1        var toast:EToast2 ?= null        fun makeText(context: Context?, message: CharSequence, HIDE_DELAY: Int): EToast2?{            if(toast == null)                toast = EToast2()            var etoast: EToast2 = toast!!            if(context == null)                throw IllegalArgumentException("You cannot show EToast2 on a null Context")            else if(Utils.isOnMainThread()&&context !is Application){                if(context is FragmentActivity){                    etoast.initDialog(etoast.get(context),HIDE_DELAY,message)                }else if(context is Activity){                    etoast.initDialog(etoast.get(context),HIDE_DELAY,message)                }            }else{                etoast.get(context)            }            return etoast        }        fun makeText(activity: Activity?,message: CharSequence,HIDE_DELAY: Int): EToast2?{            if(toast == null)                toast = EToast2()            var etoast: EToast2 = toast!!            etoast.initDialog(etoast.get(activity),HIDE_DELAY,message)            return etoast        }        fun makeText(activity: FragmentActivity?, message: CharSequence, HIDE_DELAY: Int): EToast2?{            if(toast == null)                toast = EToast2()            var etoast: EToast2 = toast!!            etoast.initDialog(etoast.get(activity),HIDE_DELAY,message)            return etoast        }        @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)        fun makeText(fragment: android.app.Fragment?, message: CharSequence, HIDE_DELAY: Int): EToast2?{            if(toast == null)                toast = EToast2()            var etoast: EToast2 = toast!!            etoast.initDialog(etoast.get(fragment),HIDE_DELAY,message)            return etoast        }        fun makeText(fragment: Fragment?, message: CharSequence, HIDE_DELAY: Int): EToast2?{            if(toast == null)                toast = EToast2()            var etoast: EToast2 = toast!!            etoast.initDialog(etoast.get(fragment),HIDE_DELAY,message)            return etoast        }    }

我们先来看下构造函数,还是通过makeText(…)只是说,这边重载了很多个不同参数的函数,也就是说,Context不限制喽~比如是Activity的话,EToast的生命周期就跟着Activity走,如果是ApplicationContext的话,那就跟着整个App生命周期走,Fragment也一样。
主要看看参数为Cotext的时候:

fun makeText(context: Context?, message: CharSequence, HIDE_DELAY: Int): EToast2?{            if(toast == null)                toast = EToast2()            var etoast: EToast2 = toast!!            if(context == null)                throw IllegalArgumentException("You cannot show EToast2 on a null Context")            else if(Utils.isOnMainThread()&&context !is Application){                if(context is FragmentActivity){                    etoast.initDialog(etoast.get(context),HIDE_DELAY,message)                }else if(context is Activity){                    etoast.initDialog(etoast.get(context),HIDE_DELAY,message)                }            }else{                etoast.get(context)            }            return etoast        }

其实就是和Glide学的,这边对context进行了分类处理喽~当然先看看是不是在主线程喽~不然的话也没意义是吧。然后说说重点,为什么Glide的所有图片都可以那么灵活聪明呢?为什么都可以跟着生命周期一起走呢?其实,当你简单看一下源码就知道了(以下是Glide的源码):

public RequestManager get(FragmentActivity activity) {        if (Util.isOnBackgroundThread()) {            return get(activity.getApplicationContext());        } else {            assertNotDestroyed(activity);            FragmentManager fm = activity.getSupportFragmentManager();            return supportFragmentGet(activity, fm);        }    }    public RequestManager get(Fragment fragment) {        if (fragment.getActivity() == null) {            throw new IllegalArgumentException("You cannot start a load on a fragment before it is attached");        }        if (Util.isOnBackgroundThread()) {            return get(fragment.getActivity().getApplicationContext());        } else {            FragmentManager fm = fragment.getChildFragmentManager();            return supportFragmentGet(fragment.getActivity(), fm);        }    }    @TargetApi(Build.VERSION_CODES.HONEYCOMB)    public RequestManager get(Activity activity) {        if (Util.isOnBackgroundThread() || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {            return get(activity.getApplicationContext());        } else {            assertNotDestroyed(activity);            android.app.FragmentManager fm = activity.getFragmentManager();            return fragmentGet(activity, fm);        }    }    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)    private static void assertNotDestroyed(Activity activity) {        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && activity.isDestroyed()) {            throw new IllegalArgumentException("You cannot start a load for a destroyed activity");        }    }    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)    public RequestManager get(android.app.Fragment fragment) {        if (fragment.getActivity() == null) {            throw new IllegalArgumentException("You cannot start a load on a fragment before it is attached");        }        if (Util.isOnBackgroundThread() || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {            return get(fragment.getActivity().getApplicationContext());        } else {            android.app.FragmentManager fm = fragment.getChildFragmentManager();            return fragmentGet(fragment.getActivity(), fm);        }    }

其实不难发现,不管你在Glide.with()方法中传入的是Activity、FragmentActivity、v4包下的Fragment、还是app包下的Fragment,最终的流程都是一样的,那就是会向当前的Activity当中添加一个隐藏的Fragment,然后你就领悟了吧~这个时候其实就有生命周期了。然后这样的话,就简单了,你只需要在Fragment中的每个生命周期回调中调用你自己定义的接口做对应的事情就OK喽 ~这样别人用起来是不是也相当方便,当生命被销毁的时候,自然就释放你的Toast的资源。其实这种方式对于你来说,以后如果想要封装一个控件,然后呢需要绑定生命周期来做一些事情或者搞事情,这种方法就很棒哈。

那么布局怎么来显示呢?EToast在之前是通过Activity最外层布局来加载需要显示的Toast的,当时呢,我觉得这样的方式挺新颖的,所以就那么做了~那么,我们这次如果不是Activity,那怎么办呢?然后我想既然要用到Fragment,那么我们能不能用DialogFragment呢?这样是不是一举两得,生命周期也有了,布局显示也有了?

所以我做了,看代码

class EToast2Fragment: DialogFragment(){    var HIDE_DELAY = 2000L    var mTextView:TextView ?= null    var message: CharSequence ?= ""    val TAG = "EToast2"    var callbask: CallBack?= null    var isShow:Boolean = false    var outAnimation: Animation ?= null    companion object{        val ANIMATION_DURATION = 500L        fun NewInstance(callback: CallBack): EToast2Fragment {            var fg = EToast2Fragment()            var bundle: Bundle = Bundle()            bundle.putSerializable(TAG,callback)            fg.arguments = bundle            return fg        }    }    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        initAnimation()        callbask = arguments.getSerializable(TAG) as CallBack?        if(callbask?.getStyle() != null)            setStyle(android.app.DialogFragment.STYLE_NORMAL, callbask?.getStyle()!!)        else            setStyle(android.app.DialogFragment.STYLE_NORMAL, android.R.style.Theme_Light_NoTitleBar)    }    fun initAnimation(){        outAnimation = AlphaAnimation(1.0f, 0.0f)        outAnimation?.duration = ANIMATION_DURATION        outAnimation?.setAnimationListener(object : Animation.AnimationListener{            override fun onAnimationRepeat(animation: Animation?) {            }            override fun onAnimationEnd(animation: Animation?) {                dismiss()            }            override fun onAnimationStart(animation: Animation?) {            }        })    }    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View {        dialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))        var view = inflater?.inflate(R.layout.etoast,null)        mTextView = view?.findViewById(R.id.mbMessage) as TextView?        mTextView?.text = message        return view!!    }    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {        view?.setOnTouchListener{ _, event ->            activity.dispatchTouchEvent(event)        }        isShow = true    }    fun setText(message: CharSequence){        this.message = message        mTextView?.text = message    }    fun delayTime(HIDE_DELAY: Int){        if (HIDE_DELAY == EToast2.LENGTH_LONG) {            this.HIDE_DELAY = 2500        } else if(HIDE_DELAY == EToast2.LENGTH_SHORT){            this.HIDE_DELAY = 1500        }    }    override fun show(manager: FragmentManager?, tag: String?) {        if(isShow){            mTextView?.removeCallbacks(mHideRunnable)        }else{            super.show(manager, tag)        }        mTextView?.postDelayed(mHideRunnable,HIDE_DELAY)    }    private val mHideRunnable = Runnable {        mTextView?.startAnimation(outAnimation)    }    override fun onDestroy() {        super.onDestroy()        callbask?.onDestroy()    }    override fun onPause() {        super.onPause()        callbask?.onPause()    }    override fun onResume() {        super.onResume()        callbask?.onResume()    }    override fun onStart() {        super.onStart()        callbask?.onStart()    }    override fun onDismiss(dialog: DialogInterface?) {        super.onDismiss(dialog)        callbask?.onDismiss()        isShow = false    }    override fun onSaveInstanceState(outState: Bundle?) {        super.onSaveInstanceState(outState)    }}

这就是常规的显示一个DialogFragment,当然还有些其他的细节,比如:

override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {        view?.setOnTouchListener{ _, event ->            activity.dispatchTouchEvent(event)        }        isShow = true    }

这边的就是为了使我们的点击穿透~当你点击DilaogFragment 的时候,点击事件还可以往下传递。然后呢,把所有的生命周期对应的方法绑定起来,也就完事儿了~当然,由于情况很多种,其实还有一个

EToast2SupportFragment

只不过是继承的包不一样而已了,其余代码是一致的。但是呢,我发现当我对Dialog设置不同显示效果的时候,的确有些不一样的事情,如果我隐藏标题,这个时候点击事件向下传递,其实透过Dialog以后,点击的位置回向上偏移50个点左右,当然,为什么引起的,大家肯定也能想到。就是标题隐藏了,导致实际位置往上走了,但是下面的Activity或Fragment的点击位置也就被偏移了。那么不隐藏标题呢,可能大家的需求不一致~又有说不清的情况。其实东西最终是写完了,目录结构如下:
这里写图片描述

但是!但是!我们再仔细想想上面我们要解决的问题哈~好像也是都解决了吧~但是最终还是被我抛弃了~我觉得是不是太臃肿了,就一个Toast以至于这么大费周折吗?主要还有点击偏移的问题,这个就不好解决了。还有dialog的style问题,那么又产生新的问题喽~好吧好吧,就当自己练练手了/(ㄒoㄒ)/~~(如果有感兴趣的,可以私聊我要源码哈~)

所以 最终,我决定从Toast源码入手,看看系统是怎么做的!我们就算不改系统的,那我们仿照系统的来做一套Toast为什么不可以呢?哈哈哈,那就开始分析源码吧。

Toast源码分析

切记看源码不要一定盯着某一行代码去拼命理解,然后卡在那里…

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {        Toast result = new Toast(context);        LayoutInflater inflate = (LayoutInflater)                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);        tv.setText(text);        result.mNextView = v;        result.mDuration = duration;        return result;    }

嗯,就是这个方法了哈~这边能看出2个信息
1. 源码真的很简单
2. Toast的布局其实就是R.layout.transient_notification

然后看show()方法

public void show() {        if (mNextView == null) {            throw new RuntimeException("setView must have been called");        }        INotificationManager service = getService();        String pkg = mContext.getOpPackageName();        TN tn = mTN;        tn.mNextView = mNextView;        try {            service.enqueueToast(pkg, tn, mDuration);        } catch (RemoteException e) {            // Empty        }    }

然后,这边来看先要拿到一个INotificationManager ,然后service.enqueueToast(pkg, tn, mDuration); 这个很关键啊,是队列!有木有,其实这样就能理解了,为什么系统的Toast是那种很讨厌的,一个接着一个慢慢的出来的,有时候点个10下,你App都退了,却还能看到一只在弹Toast。其实这东西,我们并不关注,我们主要是看看系统的Toast的是怎么显示在屏幕上的,是吧。那么继续跟踪源码,我们看看TN又是什么鬼呢?

private static class TN extends ITransientNotification.Stub {        final Runnable mHide = new Runnable() {            @Override            public void run() {                handleHide();                // Don't do this in handleHide() because it is also invoked by handleShow()                mNextView = null;            }        };        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();        final Handler mHandler = new Handler() {            @Override            public void handleMessage(Message msg) {                IBinder token = (IBinder) msg.obj;                handleShow(token);            }        };        int mGravity;        int mX, mY;        float mHorizontalMargin;        float mVerticalMargin;        View mView;        View mNextView;        int mDuration;        WindowManager mWM;        static final long SHORT_DURATION_TIMEOUT = 5000;        static final long LONG_DURATION_TIMEOUT = 1000;        TN() {            // XXX This should be changed to use a Dialog, with a Theme.Toast            // defined that sets up the layout params appropriately.            final WindowManager.LayoutParams params = mParams;            params.height = WindowManager.LayoutParams.WRAP_CONTENT;            params.width = WindowManager.LayoutParams.WRAP_CONTENT;            params.format = PixelFormat.TRANSLUCENT;            params.windowAnimations = com.android.internal.R.style.Animation_Toast;            params.type = WindowManager.LayoutParams.TYPE_TOAST;            params.setTitle("Toast");            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;        }        /**         * schedule handleShow into the right thread         */        @Override        public void show(IBinder windowToken) {            if (localLOGV) Log.v(TAG, "SHOW: " + this);            mHandler.obtainMessage(0, windowToken).sendToTarget();        }        /**         * schedule handleHide into the right thread         */        @Override        public void hide() {            if (localLOGV) Log.v(TAG, "HIDE: " + this);            mHandler.post(mHide);        }        public void handleShow(IBinder windowToken) {            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView                    + " mNextView=" + mNextView);            if (mView != mNextView) {                // remove the old view if necessary                handleHide();                mView = mNextView;                Context context = mView.getContext().getApplicationContext();                String packageName = mView.getContext().getOpPackageName();                if (context == null) {                    context = mView.getContext();                }                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);                // We can resolve the Gravity here by using the Locale for getting                // the layout direction                final Configuration config = mView.getContext().getResources().getConfiguration();                final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());                mParams.gravity = gravity;                if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {                    mParams.horizontalWeight = 1.0f;                }                if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {                    mParams.verticalWeight = 1.0f;                }                mParams.x = mX;                mParams.y = mY;                mParams.verticalMargin = mVerticalMargin;                mParams.horizontalMargin = mHorizontalMargin;                mParams.packageName = packageName;                mParams.hideTimeoutMilliseconds = mDuration ==                    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;                mParams.token = windowToken;                if (mView.getParent() != null) {                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);                    mWM.removeView(mView);                }                if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);                mWM.addView(mView, mParams);                trySendAccessibilityEvent();            }        }        private void trySendAccessibilityEvent() {            AccessibilityManager accessibilityManager =                    AccessibilityManager.getInstance(mView.getContext());            if (!accessibilityManager.isEnabled()) {                return;            }            // treat toasts as notifications since they are used to            // announce a transient piece of information to the user            AccessibilityEvent event = AccessibilityEvent.obtain(                    AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);            event.setClassName(getClass().getName());            event.setPackageName(mView.getContext().getPackageName());            mView.dispatchPopulateAccessibilityEvent(event);            accessibilityManager.sendAccessibilityEvent(event);        }                public void handleHide() {            if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);            if (mView != null) {                // note: checking parent() just to make sure the view has                // been added...  i have seen cases where we get here when                // the view isn't yet added, so let's try not to crash.                if (mView.getParent() != null) {                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);                    mWM.removeViewImmediate(mView);                }                mView = null;            }        }    }

哈哈哈~这个时候是不是真相大白了,看handleShow这边才是我们的关键,原来啊,Toast的视图是通过WindowManager的addView来加载的。那么Toast的原来总结一下就是这样:
先通过makeText()实例化出一个Toast,然后调用toast.show()方法,这时并不会马上显示Toast,而是会实例化一个TN变量,然后通过service.enqueueToast()将其加到服务队列里面去等待显示。在TN中进行调控Toast的显示格式以及里面的hide()、show()方法来控制Toast的显示和消失。然后最可悲的是这个队列是系统维护的,我们并不能干涉,所以才会出现我们屏蔽系统通知的时候,连Toast都没有了哈~

真正实现EToast2

那么既然上面做了那么多事,我们也看了Toast的源码,那么就直接仿照Google的也来写一套呗,这样不是所的问题都解决了么,不受系统控制,然后还可以把那个队列去掉~优化一下显示规则什么的,是吧。

/** * Author: Blincheng. * Date: 2017/6/30. * Description:EToast2.0 */public class EToast2 {    private WindowManager manger;    private Long time = 2000L;    private static View contentView;    private WindowManager.LayoutParams params;    private static Timer timer;    private Toast toast;    private static Toast oldToast;    public static final int LENGTH_SHORT = 0;    public static final int LENGTH_LONG = 1;    private static Handler handler;    private CharSequence text;    private EToast2(Context context, CharSequence text, int HIDE_DELAY){        manger = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);        this.text = text;        if(HIDE_DELAY == EToast2.LENGTH_SHORT)            this.time = 2000L;        else if(HIDE_DELAY == EToast2.LENGTH_LONG)            this.time = 3500L;        if(oldToast == null){            toast = Toast.makeText(context, text, Toast.LENGTH_SHORT);            contentView = toast.getView();            params = new WindowManager.LayoutParams();            params.height = WindowManager.LayoutParams.WRAP_CONTENT;            params.width = WindowManager.LayoutParams.WRAP_CONTENT;            params.format = PixelFormat.TRANSLUCENT;            params.windowAnimations = toast.getView().getAnimation().INFINITE;            params.type = WindowManager.LayoutParams.TYPE_TOAST;            params.setTitle("EToast2");            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;            params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;            params.y = 200;        }        if(handler == null){            handler = new Handler(){                @Override                public void handleMessage(Message msg) {                    EToast2.this.cancel();                }            };        }    }    public static EToast2 makeText(Context context, String text, int HIDE_DELAY){        EToast2 toast = new EToast2(context, text, HIDE_DELAY);        return toast;    }    public static EToast2 makeText(Context context, int resId, int HIDE_DELAY) {        return makeText(context,context.getText(resId).toString(),HIDE_DELAY);    }    public void show(){        if(oldToast == null){            oldToast = toast;            manger.addView(contentView, params);            timer = new Timer();        }else{            timer.cancel();            oldToast.setText(text);        }        timer = new Timer();        timer.schedule(new TimerTask() {            @Override            public void run() {                handler.sendEmptyMessage(1);            }        }, time);    }    public void cancel(){        manger.removeView(contentView);        timer.cancel();        oldToast.cancel();        timer = null;        toast = null;        oldToast = null;        contentView = null;        handler = null;    }    public void setText(CharSequence s){        toast.setText(s);    }}

上面就是EToast2的全部源码了~是不是很少,就那么点点。看构造函数,其实我们也是借用了Google的Toast,拿到里面的布局喽~这样就是原汁原味了,至少,是吧。然后其余设置其实和Toast没有什么区别,唯一不同的是没有队列了,然后显示的规则是就是:
由于在Android5.0以下无法获取是否打开系统通知权限,所以为了防止用户看不到Toast,最终的逻辑优化了一下:
1. 当用户是5.0以下的机器时,永远只显示EToast
2. 当用户是5.0以上的机器是,如果打开了通知权限,则显示系统Toast;反之则显示Etoast
3. 当一个Toast在屏幕中显示,这时又弹出Toast的话会直接改变Toast上的文字,并且重置计时器,在规定的时间后消失。

然后Toast这边我也做了一点优化,舍弃了一些接口,比如setText(Resid)。因为Toast显示的内容一般变化比较大,所以一般不会通过String写在本地吧~真的要用,我想你也有办法用的,是吧。(别说我偷懒)

/** * Author: Blincheng. * Date: 2017/6/30. * Description: */public class Toast {    private static final String CHECK_OP_NO_THROW = "checkOpNoThrow";    private static final String OP_POST_NOTIFICATION = "OP_POST_NOTIFICATION";    private static int checkNotification = 0;    private Object mToast;    private Toast(Context context, String message, int duration) {        checkNotification = isNotificationEnabled(context) ? 0 : 1;        if (checkNotification == 1) {            mToast = EToast2.makeText(context, message, duration);        } else {            mToast = android.widget.Toast.makeText(context, message, duration);        }    }    private Toast(Context context, int resId, int duration) {        if (checkNotification == -1){            checkNotification = isNotificationEnabled(context) ? 0 : 1;        }        if (checkNotification == 1 && context instanceof Activity) {            mToast = EToast2.makeText(context, resId, duration);        } else {            mToast = android.widget.Toast.makeText(context, resId, duration);        }    }    public static Toast makeText(Context context, String message, int duration) {        return new Toast(context,message,duration);    }    public static Toast makeText(Context context, int resId, int duration) {        return new Toast(context,resId,duration);    }    public void show() {        if(mToast instanceof EToast2){            ((EToast2) mToast).show();        }else if(mToast instanceof android.widget.Toast){            ((android.widget.Toast) mToast).show();        }    }    public void cancel(){        if(mToast instanceof EToast2){            ((EToast2) mToast).cancel();        }else if(mToast instanceof android.widget.Toast){            ((android.widget.Toast) mToast).cancel();        }    }    public void setText(CharSequence s){        if(mToast instanceof EToast2){            ((EToast2) mToast).setText(s);        }else if(mToast instanceof android.widget.Toast){            ((android.widget.Toast) mToast).setText(s);        }    }    /**     * 用来判断是否开启通知权限     * */    private static boolean isNotificationEnabled(Context context){        if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT){            AppOpsManager mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);            ApplicationInfo appInfo = context.getApplicationInfo();            String pkg = context.getApplicationContext().getPackageName();            int uid = appInfo.uid;            Class appOpsClass = null; /* Context.APP_OPS_MANAGER */            try {                appOpsClass = Class.forName(AppOpsManager.class.getName());                Method checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE, String.class);                Field opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION);                int value = (int)opPostNotificationValue.get(Integer.class);                return ((int)checkOpNoThrowMethod.invoke(mAppOps,value, uid, pkg) == AppOpsManager.MODE_ALLOWED);            } catch (Exception e) {                e.printStackTrace();                return false;            }        }else{            return false;        }    }}

总结

最后呢,经过这么一波三折,东西终于弄完了,这边也要特别感谢xiaogaofudao 帮忙测试和提供一些建议,非常感谢。
v2.0.1最新版本你已经上线了哦~经过本人和其他的一些用户使用体验,应该是可以的,如果大家在使用中发现什么问题,欢迎及时联系哦~(づ ̄ 3 ̄)づ

阅读全文
3 0
原创粉丝点击