Android 11适配指南之Toast解析

起源

targetSdkVersion为30的情况下,在Android 11小米10的手机上运行,调用ToastUtil的时候闪退报错:

null cannot be cast to non-null type android.widget.LinearLayout

为什么说的这么详细呢,因为这些条件都是必须的:

  • targetSdkVersion 30
  • Android 11
  • 小米10

同样的targetSdkVersion,在Android 11的华为P30 Pro上运行确实正常的,为什么呢,根据这些年厂商不断完善自己的通知渠道来看,不光我们要适配新的android版本,厂商同样也要适配,所以只能归纳为是厂商做了一些处理。

文末附Android 11适配手册

定位问题

ok,遇到问题,迅速定位。
我在原有的Toast调用上重新封装了一下,即ToastUtil

所以很快就定位到问题所在了

    private fun createToast(msg: String) {
     
        if (toast == null) {
     
            toast = Toast.makeText(YUtils.getApp().applicationContext, msg, Toast.LENGTH_SHORT)
        } else {
     
            toast!!.setText(msg)
        }
        val linearLayout = toast!!.view as LinearLayout
        val messageTextView = linearLayout.getChildAt(0) as TextView
        messageTextView.textSize = 15f
        toast!!.show()
    }

没错,就是这句进行了转换:

val linearLayout = toast!!.view as LinearLayout

代码也比较简单,拿到view之后只是设置了一下字体大小。

为什么这么写呢,且看接下来源码分析(非常简单)。

源码解析

我们一般的调用是这么写的:

Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()

一行代码,也很容易能找到重点——makeText,没错,接下来从这里开始分析

compileSdkVersion 30之前

compileSdkVersion 28为例,makeText源码:

    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
     
        Toast result = new Toast(context, looper);

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

这几行的代码重点在哪呢,在这:

View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);

引用了一个布局来显示信息

这个layout也非常的简单:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="?android:attr/toastFrameBackground">

    <TextView
        android:id="@android:id/message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:layout_marginHorizontal="24dp"
        android:layout_marginVertical="15dp"
        android:layout_gravity="center_horizontal"
        android:textAppearance="@style/TextAppearance.Toast"
        android:textColor="@color/primary_text_default_materiaal_light"/>

</LinearLayout>

根布局LinearLayoutTextView显示文本。

所以才有了前面报错的这行代码:

val linearLayout = toast!!.view as LinearLayout

现在看来其实是没有错的,事实上运行在Android11以下也确实没问题。

setViewgetView也是没问题的

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

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

author:yechaoa

compileSdkVersion 30之后

重点来了,在compileSdkVersion 30之后,源码是有改动的

还是直接看重点makeText

    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
     
        if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
     
            Toast result = new Toast(context, looper);
            result.mText = text;
            result.mDuration = duration;
            return result;
        } else {
     
            Toast result = new Toast(context, looper);
            View v = ToastPresenter.getTextToastView(context, text);
            result.mNextView = v;
            result.mDuration = duration;

            return result;
        }
    }

嗯?view的获取方式变了,原来是inflate的方式,现在是

View v = ToastPresenter.getTextToastView(context, text);

ok,继续看ToastPresenter.getTextToastView

public class ToastPresenter {
     
    ...

    @VisibleForTesting
    public static final int TEXT_TOAST_LAYOUT = R.layout.transient_notification;

    /**
     * Returns the default text toast view for message {@code text}.
     */
    public static View getTextToastView(Context context, CharSequence text) {
     
        View view = LayoutInflater.from(context).inflate(TEXT_TOAST_LAYOUT, null);
        TextView textView = view.findViewById(com.android.internal.R.id.message);
        textView.setText(text);
        return view;
    }
 }

到这里是不是有点熟悉了,没错,跟compileSdkVersion 28中的源码差不多,但是layout变成常量了,且有@VisibleForTesting注解,不过xml代码还是一样的。

而且setViewgetView也弃用的

    /**
     * Set the view to show.
     *
     * @see #getView
     * @deprecated Custom toast views are deprecated. Apps can create a standard text toast with the
     *      {@link #makeText(Context, CharSequence, int)} method, or use a
     *      Snackbar
     *      when in the foreground. Starting from Android {@link Build.VERSION_CODES#R}, apps
     *      targeting API level {@link Build.VERSION_CODES#R} or higher that are in the background
     *      will not have custom toast views displayed.
     */
    @Deprecated
    public void setView(View view) {
     
        mNextView = view;
    }

    /**
     * Return the view.
     *
     * 

Toasts constructed with {@link #Toast(Context)} that haven't called {@link #setView(View)} * with a non-{@code null} view will return {@code null} here. * *

Starting from Android {@link Build.VERSION_CODES#R}, in apps targeting API level {@link * Build.VERSION_CODES#R} or higher, toasts constructed with {@link #makeText(Context, * CharSequence, int)} or its variants will also return {@code null} here unless they had called * {@link #setView(View)} with a non-{@code null} view. If you want to be notified when the * toast is shown or hidden, use {@link #addCallback(Callback)}. * * @see #setView * @deprecated Custom toast views are deprecated. Apps can create a standard text toast with the * {@link #makeText(Context, CharSequence, int)} method, or use a * Snackbar * when in the foreground. Starting from Android {@link Build.VERSION_CODES#R}, apps * targeting API level {@link Build.VERSION_CODES#R} or higher that are in the background * will not have custom toast views displayed. */ @Deprecated @Nullable public View getView() { return mNextView; }

直接来看注释的重点:

@deprecated Custom toast views are deprecated. Apps can create a standard text toast with the
{@link #makeText(Context, CharSequence, int)} method, or use a Snackbar
when in the foreground. Starting from Android {@link Build.VERSION_CODES#R}, apps
targeting API level {@link Build.VERSION_CODES#R} or higher that are in the background
will not have custom toast views displayed.

大意:
自定义toast view已经弃用,你可以创建一个标准的toast,或者用Snackbar
从AndroidR开始,将不再显示自定位toast view。

Android R 也就是Android11,具体的版本对应关系查看

这里有同学可能会有一些想法,既然getView弃用了,那我可不可以像系统一样通过ToastPresenter.getTextToastView来获取呢,很遗憾,是不行的,ToastPresenter@hide。。

适配方案

综上所诉,适配方案也了然于心了。

方案一

使用标准的toast

Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()

方案二

使用Snackbar

Snackbar的使用跟Toast差不多,更多查看这个。

Snackbar.make(view, "已加入行程", Snackbar.LENGTH_SHORT).show()

方案三

不使用系统的toast,但可以借鉴来写一个自定义view

大致思路:

  • 初始化引用自定义布局
  • 编写一些公开的set、get属性
  • 加上进入进出动画
  • 开始/结束显示倒计时

等有空了再来补一下这个。。

Android 11开发手册

《Android 11 开发者手册》

最后

写作不易,如果对你有用,点个赞呗 ^ _ ^

你可能感兴趣的:(Android,知识点,Android,疑难杂症,android11,toast,android适配,android)