Android问题——解决Toast在APP通知关闭不显示

Android在8.0中针对通知一块在功能上做了较大的改动。
Toast被纳入到了通知管理,其实这在Android 4.4(API 19)以上就已经有特别的处理了。只是一般用户不会怎么注意,开发人员也不会很在意APP的通知开关,因为GOOGLE还没有在通知上大做文章,进入到APP信息中,通知的开关也不是很起眼。但是8.0上针对通知部分(主要针对下拉通知)做了较大修改,其中牵连到Toast,且APP信息也种通知开发内选项也丰富了。

因此在APP设置中关闭通知开关后,Toast也会有不显示的情况——这个主要是针对国内不同的厂商定制,表现各有不同。

标准API中可以通过

android.support.v4.app.NotificationManagerCompat

的方法

/**
 * Returns whether notifications from the calling package are not blocked.
 */
public boolean areNotificationsEnabled ()

判断通知是否阻止。

这个方法里的实现分成了3段,Android 4.4之前方法永远是true,在Android 4.4 ~ Android 7通过AppOpsManager来判断,在Android 7以上最终调用的android.app.NotificationManager进行的判断。

至于为什么关闭通知开关后不显示Toast,需要跟踪到NotificationManagerService的源码查看enqueueToast方法,这里不具体说,主要说说如何解决。


先看看各机型测试的情况

机型Android版本areNotificationsEnabledSDK_INTToast类型是否显示
OPPO R158.1.0false27Customized Toast
MI NOTE LTE6.0.1false23Android OS Toast
NUBIA Z11 mini5.1.1false22Android OS Toast
HW MATE 76.0false23Android OS Toast

小米显示TYPE_TOAST机型
机型Android版本MIUI版本areNotificationEnabledSDK_INTToast显示是否显示备注
MI MOTE LTE6.0MIUI 9.2稳定版false23Custimized Toast正常显示
Redmi 3s6.0MIUI 7.3稳定版false23Customized Toast正常显示
MI 4c5.1.1MIUI 8.2稳定版false22Customized Toast虽然显示,但是不正常,点击N次只显示一两次
MI 58.0.0MIUI 10 8.11.22开发版false26Customized Toast正常显示
MI 89MIUI 10.1稳定版false28Customized Toast正常显示
Redmi Note46.0MIUI 10.2稳定版false23Android OS Toast正常显示

结果: MIUI 8以下Toast不显示,在MIUI论坛上可以看到这是因为MIUI不支持TYPE_TOAST,因此无法显示。

####小米,华为等关闭通知,系统Toast显示情况。

机型Android版本MIUI版本areNotificationEnabledSDK_INTToast显示是否显示备注
MI MOTE LTE6.0MIUI 9.2稳定版false23Android OS Toast正常显示
MI 34.4.4MIUI 9.2稳定版false19Android OS Toast正常显示
MI 4C5.1.1MIUI 8.2稳定版false22Android OS Toast-
MI 58.0.0MIUI 10 8.00.22开发版false26Android OS Toast-
MI 89MIUI 10.1稳定版false28Android OS Toast-
Redmi 3s6.0.1MIUI 7.3稳定版false23Android OS Toast-
Redmi Note46.0MIUI 10.2稳定版false23Android OS Toast-
华为荣耀64.4.2-false19Android OS Toast-
华为荣耀75.0.2-false21Android OS Toast-
华为Mate 109-false28Android OS Toast-
魅族 MX4 PRO5.1.1-false22Android OS Toast-
魅族 MX67.1.1-false25Android OS Toast-
OPPO R158.1.0-false27Android OS Toast-
OPPO R11 PLUS8.1.0-false27Android OS Toast-
OPPO R74.4.4-false19Android OS Toast-
OPPO R9 PLUS5.1.1-false22Android OS Toast-
努比亚Z11 mini5.1.1-false22Android OS Toast-

MIUI显示与其他品牌手机Toast显示有区别。
OPPO R15也显示了Toast。

看看自定义Toast

根据系统Toast进行修改,由于系统Toast中调用了NotificationManagerService的接口,因此将涉及的部分先进行删除。第二,Toast在NotificationManagerService中是被进行排队处理,因此删除了NotificationManagerService的调用部分后,需要自己定义队列管理。

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.TextView;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

import static android.os.Build.VERSION.SDK_INT;

/**
 * A toast is a view containing a quick little message for the user.  The toast class
 * helps you create and show those.
 *
 * <p>
 * When the view is shown to the user, appears as a floating view over the
 * application.  It will never receive focus.  The user will probably be in the
 * middle of typing something else.  The idea is to be as unobtrusive as
 * possible, while still showing the user the information you want them to see.
 * Two examples are the volume control, and the brief message saying that your
 * settings have been saved.
 * <p>
 * The easiest way to use this class is to call one of the static methods that constructs
 * everything you need and returns a new Toast object.
 *
 * <div class="special reference">
 * <h3>Developer Guides</h3>
 * <p>For information about creating Toast notifications, read the
 * <a href="{@docRoot}guide/topics/ui/notifiers/toasts.html">Toast Notifications</a> developer
 * guide.</p>
 * </div>
 */
class Toast {
    private static final String TAG = "Toast";
    private static final boolean localLOGV = false;


    /**
     * 维护toast的队列
     */
    private static BlockingQueue<TN> mQueue = new LinkedBlockingQueue<TN>();

    /**
     * 原子操作:判断当前是否在读取{**@linkplain **#mQueue 队列}来显示toast
     */
    private static AtomicInteger mAtomicInteger = new AtomicInteger(0);

    @Retention(RetentionPolicy.SOURCE)
    @interface Duration {
    }

    /**
     * Show the view or text notification for a short period of time.  This time
     * could be user-definable.  This is the default.
     *
     * @see #setDuration
     */
    public static final int LENGTH_SHORT = 3000;

    /**
     * Show the view or text notification for a long period of time.  This time
     * could be user-definable.
     *
     * @see #setDuration
     */
    public static final int LENGTH_LONG = 5000;

    private final Context mContext;
    private final TN mTN;
    private long mDuration;
    private View mNextView;

    private final static Handler mHandler = new Handler();

    private static Runnable mActive = new Runnable() {
        @Override
        public void run() {
            activeQueue();
        }
    };

    private static void activeQueue() {
        final TN tn = mQueue.peek();
        if (tn == null) {
            mAtomicInteger.decrementAndGet();
            return;
        }
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                tn.show();
            }
        });
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                if (tn.mNextView != null && tn.mNextView.getParent() != null) {
                    mQueue.poll();
                }
                tn.hide();
            }
        }, tn.mDuration);
        mHandler.postDelayed(mActive, tn.mDuration);
    }

    /**
     * Construct an empty Toast object.  You must call {@link #setView} before you
     * can call {@link #show}.
     *
     * @param context The context to use.  Usually your {@link android.app.Application}
     *                or {@link android.app.Activity} object.
     */
    public Toast(Context context) {
        this(context, null);
    }

    /**
     * Constructs an empty Toast object.  If looper is null, Looper.myLooper() is used.
     */
    public Toast(@NonNull Context context, @Nullable Looper looper) {
        mContext = context;
        mTN = new TN(context.getPackageName(), looper);
        mTN.mY = dip2px(context, 26);
        mTN.mGravity = 0x00000051;
    }

	public int dip2px(Context context,float dpValue) {
	    final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale +0.5f);
    }

    /**
     * Show the view for the specified duration.
     */
    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        TN tn = mTN;
        tn.mNextView = mNextView;

        mQueue.offer(tn);
        if (mAtomicInteger.get() == 0) {
            mAtomicInteger.incrementAndGet();
            mHandler.post(mActive);
        }
    }

    /**
     * Close the view if it's showing, or don't show it if it isn't showing yet.
     * You do not normally have to call this.  Normally view will disappear on its own
     * after the appropriate duration.
     */
    public void cancel() {
        if (0 == mAtomicInteger.get() && mQueue.isEmpty()) {
            return;
        }

        final TN tn = mQueue.peek();
        if (mTN == tn) {
            mHandler.removeCallbacks(mActive);
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (tn != null) {
                        tn.cancel();
                    }
                }
            });
            mHandler.post(mActive);
        }
    }

    /**
     * Set the view to show.
     *
     * @see #getView
     */
    public void setView(View view) {
        mNextView = view;
    }

    /**
     * Return the view.
     *
     * @see #setView
     */
    public View getView() {
        return mNextView;
    }

    /**
     * Set how long to show the view for.
     *
     * @see #LENGTH_SHORT
     * @see #LENGTH_LONG
     */
    public void setDuration(@Duration int duration) {
        mDuration = duration;
        mTN.mDuration = duration;
    }

    /**
     * Return the duration.
     *
     * @see #setDuration
     */
    @Duration
    public long getDuration() {
        return mDuration;
    }

    /**
     * Set the margins of the view.
     *
     * @param horizontalMargin The horizontal margin, in percentage of the
     *                         container width, between the container's edges and the
     *                         notification
     * @param verticalMargin   The vertical margin, in percentage of the
     *                         container height, between the container's edges and the
     *                         notification
     */
    public void setMargin(float horizontalMargin, float verticalMargin) {
        mTN.mHorizontalMargin = horizontalMargin;
        mTN.mVerticalMargin = verticalMargin;
    }

    /**
     * Return the horizontal margin.
     */
    public float getHorizontalMargin() {
        return mTN.mHorizontalMargin;
    }

    /**
     * Return the vertical margin.
     */
    public float getVerticalMargin() {
        return mTN.mVerticalMargin;
    }

    /**
     * Set the location at which the notification should appear on the screen.
     *
     * @see android.view.Gravity
     * @see #getGravity
     */
    public void setGravity(int gravity, int xOffset, int yOffset) {
        mTN.mGravity = gravity;
        mTN.mX = xOffset;
        mTN.mY = yOffset;
    }

    /**
     * Get the location at which the notification should appear on the screen.
     *
     * @see android.view.Gravity
     * @see #getGravity
     */
    public int getGravity() {
        return mTN.mGravity;
    }

    /**
     * Return the X offset in pixels to apply to the gravity's location.
     */
    public int getXOffset() {
        return mTN.mX;
    }

    /**
     * Return the Y offset in pixels to apply to the gravity's location.
     */
    public int getYOffset() {
        return mTN.mY;
    }

    /**
     * Gets the LayoutParams for the Toast window.
     *
     * @hide
     */
    public WindowManager.LayoutParams getWindowParams() {
        return mTN.mParams;
    }

    /**
     * Make a standard toast that just contains a text view.
     *
     * @param context  The context to use.  Usually your {@link android.app.Application}
     *                 or {@link android.app.Activity} object.
     * @param text     The text to show.  Can be formatted text.
     * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or
     *                 {@link #LENGTH_LONG}
     */
    public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        return makeText(context, null, text, duration);
    }

    /**
     * Make a standard toast to display using the specified looper.
     * If looper is null, Looper.myLooper() is used.
     *
     * @hide
     */
    @SuppressLint("ShowToast")
    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
                                 @NonNull CharSequence text, @Duration int duration) {
        Toast result = new Toast(context, looper);

        View v = android.widget.Toast.makeText(context, text, duration).getView();
        TextView tv = v.findViewById(android.R.id.message);
        tv.setText(text);

        result.mNextView = v;
        result.mDuration = duration;

        return result;
    }

    /**
     * Make a standard toast that just contains a text view with the text from a resource.
     *
     * @param context  The context to use.  Usually your {@link android.app.Application}
     *                 or {@link android.app.Activity} object.
     * @param resId    The resource id of the string resource to use.  Can be formatted text.
     * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or
     *                 {@link #LENGTH_LONG}
     * @throws Resources.NotFoundException if the resource can't be found.
     */
    public static Toast makeText(Context context, @StringRes int resId, @Duration int duration)
            throws Resources.NotFoundException {
        return makeText(context, context.getResources().getText(resId), duration);
    }

    /**
     * Update the text in a Toast that was previously created using one of the makeText() methods.
     *
     * @param resId The new text for the Toast.
     */
    public void setText(@StringRes int resId) {
        setText(mContext.getText(resId));
    }

    /**
     * Update the text in a Toast that was previously created using one of the makeText() methods.
     *
     * @param s The new text for the Toast.
     */
    public void setText(CharSequence s) {
        if (mNextView == null) {
            throw new RuntimeException("This Toast was not created with Toast.makeText()");
        }
        TextView tv = mNextView.findViewById(android.R.id.message);
        if (tv == null) {
            throw new RuntimeException("This Toast was not created with Toast.makeText()");
        }
        tv.setText(s);
    }


    private static class TN {
        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();

        private static final int SHOW = 0;
        private static final int HIDE = 1;
        private static final int CANCEL = 2;
        final Handler mHandler;

        int mGravity;
        int mX;
        int mY;
        float mHorizontalMargin;
        float mVerticalMargin;

        View mView;
        View mNextView;
        long mDuration = SHORT_DURATION_TIMEOUT;

        WindowManager mWM;

        String mPackageName;

        static final long SHORT_DURATION_TIMEOUT = 2000;
        static final long LONG_DURATION_TIMEOUT = 5000;

        TN(String packageName, @Nullable Looper looper) {
            // 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 = android.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;

            mPackageName = packageName;

            if (looper == null) {
                // Use Looper.myLooper() if looper is not specified.
                looper = Looper.myLooper();
                if (looper == null) {
                    throw new RuntimeException(
                            "Can't toast on a thread that has not called Looper.prepare()");
                }
            }
            mHandler = new Handler(looper, null) {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case SHOW: {
                            handleShow();
                            break;
                        }
                        case HIDE: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            break;
                        }
                        case CANCEL: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            break;
                        }
                    }
                }
            };
        }

        /**
         * schedule handleShow into the right thread
         */
        public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(SHOW).sendToTarget();
        }

        /**
         * schedule handleHide into the right thread
         */
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.obtainMessage(HIDE).sendToTarget();
        }

        public void cancel() {
            if (localLOGV) Log.v(TAG, "CANCEL: " + this);
            mHandler.obtainMessage(CANCEL).sendToTarget();
        }

        public void handleShow() {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            // If a cancel/hide is pending - no need to show - at this point
            // the window token is already invalid and no need to do any work.
            if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
                return;
            }
            if (mView != mNextView) {
                // remove the old view if necessary
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mPackageName;
                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();
                int gravity = mGravity;
                if (SDK_INT > 16) {
                    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;

                if (SDK_INT > 23) {
                    try {
                        Class clazz = Class.forName("android.view.WindowManager");
                        Field field = clazz.getDeclaredField("hideTimeoutMilliseconds");
                        field.setAccessible(true);
                        field.setLong(mParams, mDuration ==
                                Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT);
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    } catch (NoSuchFieldException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }

                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);
                // Since the notification manager service cancels the token right
                // after it notifies us to cancel the toast there is an inherent
                // race and we may attempt to add a window after the token has been
                // invalidated. Let us hedge against that.
                try {
                    mWM.addView(mView, mParams);
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            }
        }

        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;
            }
        }
    }
}

这个类完全可用,需要使用可以拷贝直接使用

针对MIUI 8需要在WindowManager.addView前后添加标记设置。
                //...
                try {
                    final int miuiVersion = MobileManufacturer.miuiVersion();
                    if (miuiVersion <= 8) {
                        MobileManufacturer.setMIUIInternational(true);
                    }
                    mWM.addView(mView, mParams);
                    if (miuiVersion <= 8) {
                        MobileManufacturer.setMIUIInternational(false);
                    }
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
               // ...
针对不同厂商需要可能需要不同适配,因此重新定义一个判断厂商工具类。
// MobileManufacturer.java
import android.annotation.SuppressLint;
import android.os.Build;
import android.text.TextUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

final class MobileManufacturer {
    static boolean isMIUI() {
        return TextUtils.equals(Build.MANUFACTURER, "Xiaomi");
    }

    static int miuiVersion() {
        return getMIUIVersion();
    }

    static void setMIUIInternational(boolean flag) {
        try {
            final Class BuildForMi = Class.forName("miui.os.Build");
            final Field isInternational = BuildForMi.getDeclaredField("IS_INTERNATIONAL_BUILD");
            isInternational.setAccessible(true);
            isInternational.setBoolean(null, flag);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static int getMIUIVersion() {
        try {
            @SuppressLint("PrivateApi")
            Class<?> sysClass = Class.forName("android.os.SystemProperties");
            Method getStringMethod = sysClass.getDeclaredMethod("get", String.class);
            final String version = (String) getStringMethod.invoke(sysClass, "ro.miui.ui.version.name");
            if (!TextUtils.isEmpty(version)) {
                return Integer.valueOf(version.substring(1));

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 6;
    }
}

这里主要采用了MIUI做实验对比,若不同厂商定制的手机不同而导致不显示Toast,均可采用以上自定义的Toast进行显示。

查看厂商ROM

toast 展示的内容过多时,可以通过设置 Toast显示时长和位置,以及使用自定义的 Toast 布局来解决省略号的问题。 1. 设置 Toast显示时长和位置 可以通过Toast类的 setDuration() 方法来设置 Toast 的展示时长,如下所示: ``` Toast toast = Toast.makeText(context, message, Toast.LENGTH_LONG); toast.setDuration(5000); // 5秒钟 ``` 可以将 duration 设置为一个较长的时间,来确保用户足够的时间看清楚 Toast 显示的内容。 另外,当 Toast 显示的内容过多时,可以通过 setGravity() 方法来设置 Toast显示位置,如下所示: ``` toast.setGravity(Gravity.CENTER, 0, 0); ``` 上述代码会将 Toast 居中显示。通过调整 setGravity() 方法的参数,可以将 Toast 显示在其他位置。 2. 使用自定义的 Toast 布局 可以通过 LayoutInflater 类来加载自定义的 Toast 布局,并在其中显示需要展示的内容。自定义布局可以是一个 XML 文件,也可以是一个 View 对象。 以下是一个使用自定义布局的例子: ``` LayoutInflater inflater = getLayoutInflater(); View layout = inflater.inflate(R.layout.custom_toast, (ViewGroup) findViewById(R.id.custom_toast_layout)); TextView text = (TextView) layout.findViewById(R.id.text); text.setText("需要显示的内容"); Toast toast = new Toast(getApplicationContext()); toast.setDuration(Toast.LENGTH_LONG); toast.setView(layout); toast.show(); ``` 在上述代码中,我们使用自定义布局 custom_toast.xml,并在其中添加一个 TextView 来显示需要展示的内容。然后,我们将该布局设置为 Toast 的 View,以此来展示自定义的 Toast。 通过使用自定义的 Toast 布局,我们可以自由地控制 Toast 的样式和显示内容,从而更好地展示需要展示的内容。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

VoidHope

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值