美文网首页
Toast源码学习

Toast源码学习

作者: 留给时光吧 | 来源:发表于2018-04-03 20:24 被阅读0次

Toast的作用主要是快速的展示相关信息给用户,同时又不占据太多屏幕位置,也不夺取焦点,更重要的时其调用非常简单,一行代码就可以实现。所以学习一下Toast的源码还是很有必要的。
Toast的源码在这个位置:

frameworks\base\core\java\android\widget\Toast.java

先看我们常用的调用方式

Toast.makeText(context,"Hello ",Toast.LENGTH_SHORT).show();

一段非常简洁的链式调用代码,一个方法就是设置内容和时长,第二个方法就是显示。
我们先从第一个方法入手。用过的都知道makeText有一个重载方法,主要就是所传内容的参数类型不同,一个可以直接传入字符串,一个则可以传入资源ID,如下:

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        ...
}

public static Toast makeText(Context context, @StringRes int resId, @Duration int duration)
                                throws Resources.NotFoundException {
        ...
}

但是从8.0开始,又多了一个重载方法,如下:

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

可以看到多了一个Looper参数,用过的朋友都知道Toast是不可以直接在子线程调用的,否则会有如下错误:

Can't toast on a thread that has not called Looper.prepare()

具体原因后面在分析,根据其提示信息,我们可以分别调用 Looper.prepare()和 Looper.loop()来实现在子线程使用Toast。但这样比较麻烦,所以8.0中又多了一个这样的方法,可以直接传入一个主线程的looper,然后在子线程调用Toast。但是这个方法目前是hide状态的,我们无法直接调用,为了试试这个方法,我们可以通过反射调用一下,看看效果。

final Class clazz = Class.forName("android.widget.Toast");
final Method method = clazz.getMethod("makeText", Context.class, Looper.class,CharSequence.class,int.class);
new Thread(){
         @Override
         public void run() {
              super.run();
              try {
                 Toast toast = (Toast) method.invoke(null,context,Looper.getMainLooper(),"Hello",0);
                 toast.show();
              } catch (Exception e) {
                  Log.d("MTC",e.getMessage());
              }
         }
}.start();

结果证明是可以正常调用的,至于为什么要隐藏这个方法,目前还不清楚。
现在我们开始看这个方法的具体实现:

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        return makeText(context, null, text, duration);
}

public static Toast makeText(Context context, @StringRes int resId, @Duration int duration)
                                throws Resources.NotFoundException {
        return makeText(context, context.getResources().getText(resId), duration);
}

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

可以看到前两个makeText方法都在间接调用第三个重载,只是默认将looper方法设为null而已。在第三个重载中,主要做了这几件事,1.构造一个Toast对象。2.引入一个布局并且给Textview设置内容。3.设置显示时间。

相应的在Toast中也提供了一些get,set方法来获取和设置布局和文字。如:setView,getView,setText。注意没有getText方法。其中有setDuration方法,但是并不是想象中那样自定义事件,而是只能设置为LENGTH_SHORT和LENGTH_LONG两种。对应的也有getDuration。

我们可以简单看一下这个布局文件,布局很简单,就是一个TextView

frameworks\base\core\res\res\layout\transient_notification.xml

<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_material_light"
        />

</LinearLayout>

接下来看一下Toast的构造方法:

public Toast(Context context) {
        this(context, null);
}

public Toast(@NonNull Context context, @Nullable Looper looper) {
        mContext = context;
        mTN = new TN(context.getPackageName(), looper);
        mTN.mY = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.toast_y_offset);
        mTN.mGravity = context.getResources().getInteger(
                com.android.internal.R.integer.config_toastDefaultGravity);
}

同样,相对于之前版本的代码,8.0中也多出了一个带Looper参数的构造函数,同样这个构造函数也是隐藏的。第一个构造也是调用第二个构造。在构造函数中,初始化了Context变量,构造了一个TN对象,并设置TN对象中的一些参数。我们浏览Toast一些布局设置的方法时发现,比如setGravity,setMargin等,都是间接的设置给了TN,说明TN是用来控制Toast的。我们就来看一下这个类。

它是Toast里的一个静态内部类,父类是ITransientNotification,构造函数如下:

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

            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: {
                            IBinder token = (IBinder) msg.obj;
                            handleShow(token);
                            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;
                            try {
                                getService().cancelToast(mPackageName, TN.this);
                            } catch (RemoteException e) {
                            }
                            break;
                        }
                    }
                }
            };
}

在构造中,先是设置许多布局属性,在这里我们看到在设置flag时设置为不可触摸和不可获得焦点。然后设置looper变量,如果使用不带Looper参数的makeText方法,这里的looper会用Looper.myLooper()方法初始化,这也就是在子线程中,myLooper()返回为空,导致报错的原因。这个looper使用在Handler中的,所以不能为空。之后实例化了一个Handler对象,用于处理show,hide和cancel动作。

在TN里面也定义了显示的时长:

static final long SHORT_DURATION_TIMEOUT = 4000;
static final long LONG_DURATION_TIMEOUT = 7000;

我们也看到TN里的show,hide,cancel方法也都是通过mHandler传递消息,在Handler对象调用对应方法实现的。

        @Override
        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
        }

        @Override
        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();
        }

我们首先看一下handleShow方法:

public void handleShow(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
                return;
            }
            if (mView != mNextView) {
                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);
                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);
                try {
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            }
        }

可以看出还是很简单的,主要是设置窗口参数,然后通过WindowManager添加视图即可。由此可以想到hide就是通过WindowManager移除视图,具体看代码:

public void handleHide() {
            if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
            if (mView != null) {
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeViewImmediate(mView);
                }

                mView = null;
            }
        }

这里总结一下,Toast的makeText方法只是实例化出来一个Toast对象和TN对象,其中Toast只是用来提供接口让我们设置各种参数,TN则是实际上用来控制Toast的显示隐藏及布局等操作。

最后看一下Toast的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对象,然后将显示toast的请求加入队列,等待显示。
INotificationManager的源码在以下位置:

frameworks\base\services\core\java\com\android\server\notification\NotificationManagerService.java

由于Toast的显示和隐藏是由INotificationManager管理的,所以我们具体看一下相关的几个方法。

@Override
        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
            if (pkg == null || callback == null) {
                Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
                return ;
            }
            final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
            final boolean isPackageSuspended =
                    isPackageSuspendedForUser(pkg, Binder.getCallingUid());

            if (ENABLE_BLOCKED_TOASTS && !isSystemToast &&
                    (!areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())
                            || isPackageSuspended)) {
                return;
            }

            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();
                long callingId = Binder.clearCallingIdentity();
                try {
                    ToastRecord record;
                    int index;
                    if (!isSystemToast) {
                        index = indexOfToastPackageLocked(pkg);
                    } else {
                        index = indexOfToastLocked(pkg, callback);
                    }
                    if (index >= 0) {
                        record = mToastQueue.get(index);
                        record.update(duration);
                        record.update(callback);
                    } else {
                        Binder token = new Binder();
                        mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                    }
                    keepProcessAliveIfNeededLocked(callingPid);
                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }

这就是进队列的方法,其中mToastQueue就是一个ArrayList,里面保存着一个个ToastRecord,ToastRecord也是一个静态内部类,负责保存进程ID,ITransientNotification实例,时长等信息。基本流程就是先判断是否存在队列中(这个判断主要是基于包名的,详见indexOfToastPackageLocked方法),若存在则更新时长和TN信息(用于更新内容等),否则加入队列末尾。若当前是队列第一个,则调用showNextToastLocked()来显示,方法如下:

void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            try {
                record.callback.show(record.token);
                scheduleTimeoutLocked(record);
                return;
            } catch (RemoteException e) {
                int index = mToastQueue.indexOf(record);
                if (index >= 0) {
                    mToastQueue.remove(index);
                }
                keepProcessAliveIfNeededLocked(record.pid);
                if (mToastQueue.size() > 0) {
                    record = mToastQueue.get(0);
                } else {
                    record = null;
                }
            }
        }
    }

基本流程就是不断从队列中取第一个ToastRecord,然后调用TN实例中的show方法显示。也就回到之前我们看的handleShow方法中,利用WindowManager添加视图进行显示。之后调用了scheduleTimeoutLocked,主要用于移除Toast

private void scheduleTimeoutLocked(ToastRecord r)
    {
        mHandler.removeCallbacksAndMessages(r);
        Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
        long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        mHandler.sendMessageDelayed(m, delay);
    }

在这里我们看到了Toast显示时长的实现,就是发送一个延迟消息,延迟期间就是显示的时机。当Handler收到MESSAGE_TIMEOUT消息时,执行下面方法:

private void handleTimeout(ToastRecord record)
    {
        synchronized (mToastQueue) {
            int index = indexOfToastLocked(record.pkg, record.callback);
            if (index >= 0) {
                cancelToastLocked(index);
            }
        }
    }

又调用了cancelToastLocked方法,附带该Toast在队列的位置:

void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        try {
            record.callback.hide();
        } catch (RemoteException e) {
        }

        ToastRecord lastToast = mToastQueue.remove(index);
        mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);

        keepProcessAliveIfNeededLocked(record.pid);
        if (mToastQueue.size() > 0) {
            showNextToastLocked();
        }
    }

可见显示调用了TN中的hide方法,然后将ToastRecord移出队列,然后循环去显示下一个Toast。

相关文章

网友评论

      本文标题:Toast源码学习

      本文链接:https://www.haomeiwen.com/subject/kvgchftx.html