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版本 | areNotificationsEnabled | SDK_INT | Toast类型 | 是否显示 |
---|---|---|---|---|---|
OPPO R15 | 8.1.0 | false | 27 | Customized Toast | 是 |
MI NOTE LTE | 6.0.1 | false | 23 | Android OS Toast | 是 |
NUBIA Z11 mini | 5.1.1 | false | 22 | Android OS Toast | 否 |
HW MATE 7 | 6.0 | false | 23 | Android OS Toast | 否 |
小米显示TYPE_TOAST机型
机型 | Android版本 | MIUI版本 | areNotificationEnabled | SDK_INT | Toast显示 | 是否显示 | 备注 |
---|---|---|---|---|---|---|---|
MI MOTE LTE | 6.0 | MIUI 9.2稳定版 | false | 23 | Custimized Toast | 是 | 正常显示 |
Redmi 3s | 6.0 | MIUI 7.3稳定版 | false | 23 | Customized Toast | 是 | 正常显示 |
MI 4c | 5.1.1 | MIUI 8.2稳定版 | false | 22 | Customized Toast | 是 | 虽然显示,但是不正常,点击N次只显示一两次 |
MI 5 | 8.0.0 | MIUI 10 8.11.22开发版 | false | 26 | Customized Toast | 是 | 正常显示 |
MI 8 | 9 | MIUI 10.1稳定版 | false | 28 | Customized Toast | 是 | 正常显示 |
Redmi Note4 | 6.0 | MIUI 10.2稳定版 | false | 23 | Android OS Toast | 是 | 正常显示 |
结果: MIUI 8以下Toast不显示,在MIUI论坛上可以看到这是因为MIUI不支持TYPE_TOAST,因此无法显示。
####小米,华为等关闭通知,系统Toast显示情况。
机型 | Android版本 | MIUI版本 | areNotificationEnabled | SDK_INT | Toast显示 | 是否显示 | 备注 |
---|---|---|---|---|---|---|---|
MI MOTE LTE | 6.0 | MIUI 9.2稳定版 | false | 23 | Android OS Toast | 是 | 正常显示 |
MI 3 | 4.4.4 | MIUI 9.2稳定版 | false | 19 | Android OS Toast | 是 | 正常显示 |
MI 4C | 5.1.1 | MIUI 8.2稳定版 | false | 22 | Android OS Toast | 否 | - |
MI 5 | 8.0.0 | MIUI 10 8.00.22开发版 | false | 26 | Android OS Toast | 是 | - |
MI 8 | 9 | MIUI 10.1稳定版 | false | 28 | Android OS Toast | 是 | - |
Redmi 3s | 6.0.1 | MIUI 7.3稳定版 | false | 23 | Android OS Toast | 否 | - |
Redmi Note4 | 6.0 | MIUI 10.2稳定版 | false | 23 | Android OS Toast | 是 | - |
华为荣耀6 | 4.4.2 | - | false | 19 | Android OS Toast | 否 | - |
华为荣耀7 | 5.0.2 | - | false | 21 | Android OS Toast | 否 | - |
华为Mate 10 | 9 | - | false | 28 | Android OS Toast | 否 | - |
魅族 MX4 PRO | 5.1.1 | - | false | 22 | Android OS Toast | 否 | - |
魅族 MX6 | 7.1.1 | - | false | 25 | Android OS Toast | 否 | - |
OPPO R15 | 8.1.0 | - | false | 27 | Android OS Toast | 是 | - |
OPPO R11 PLUS | 8.1.0 | - | false | 27 | Android OS Toast | 是 | - |
OPPO R7 | 4.4.4 | - | false | 19 | Android OS Toast | 否 | - |
OPPO R9 PLUS | 5.1.1 | - | false | 22 | Android OS Toast | 是 | - |
努比亚Z11 mini | 5.1.1 | - | false | 22 | Android 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.
*
*
* 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.
*
* 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.
*
*
* Developer Guides
* For information about creating Toast notifications, read the
* Toast Notifications developer
* guide.
*
*/
class Toast {
private static final String TAG = "Toast";
private static final boolean localLOGV = false;
/**
* 维护toast的队列
*/
private static BlockingQueue mQueue = new LinkedBlockingQueue();
/**
* 原子操作:判断当前是否在读取{**@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;
}
}
}
}
这个类完全可用,需要使用可以拷贝直接使用
//...
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进行显示。