重新梳理一下自定义Toast的用法,设置想要的位置,自定义宽和高,自定义显示时间等

之前在自定义Toast的过程中走了不少弯路,不是去折腾其子布局,就是通过别人的方法设置了却不知道是什么原理。具体可参照一下之前记录的文档

今天看了下源码,重新梳理了一下。

目前我需要自定义Toast的使用场景:

需要一个横向内容充满屏幕的Toast(默认的Toast一般出现在屏幕的中间靠下方的位置,并且只是一小块区域,文字多了的时候会换行,实际希望是不换行,单行显示充满屏幕),连续点击button的时候,希望只保持一个Toast实例(如果不做特殊处理,每次点击button都会重新创建一个Toast,快速连续点击之后,就会出现虽然停止点击了,但是taost一直显示着,并且每个toast接替显示会产生闪烁的效果)。

先解决全屏的问题。

首先我们先来看看正常使用toast的示例:

Toast.makeText(this,"abc",Toast.LENGTH_LONG).show();
跟进源码看下makeText做了什么:

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;
    }
这个静态方法初始化了一个toast示例,然后通过layoutInflater实例化了一个默认的布局文件,并设置了默认的显示时长。
而我们自定义Toast时是怎么做的呢?

public static void showToastSmall(Activity act, String str, int length) {
	 // TODO Auto-generated method stub
	 LayoutInflater inflater = act.getLayoutInflater();
	 View layout = inflater.inflate(R.layout.toast_submit_small, null);
	
	 TextView text = (TextView) layout.findViewById(R.id.tv_toast_small);
	 text.setText(str);
	
	 if (sToast == null) {
	 sToast = new Toast(act.getApplicationContext());
	 }
	
	 sToast.setGravity(Gravity.BOTTOM, 0, -75);
	 sToast.setDuration(length);
	 sToast.setView(layout);
	 sToast.show();
	 }
其实也就是把我们自己定义的布局文件,加载进来,通过toast的setView方法,把自定义的布局view设置给toast的变量mNextView。

在这里我想要实现全屏的效果,原来的做法是直接设置自定义布局为全屏的,但是实际效果不是这样的。原因是toast的mNextView实际上是附着在一个WindowManager上的,

这个WindowManager才是真正控制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
        }
    }
show方法里面拿到了一个service示例,猜想这个是用来把toast加入到服务器队列的,这个队列里面还传入了一个TN对象,看TN对象是在Toast构造里面初始化的:

public Toast(Context context) {
        mContext = context;
        mTN = new TN();
        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);
    }
跟进这个TN对象中:

 private static class TN extends ITransientNotification.Stub {
        final Runnable mShow = new Runnable() {
            @Override
            public void run() {
                handleShow();
            }
        };

        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();    

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


        View mView;
        View mNextView;

        WindowManager mWM;

        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() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.post(mShow);
        }

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

        public void handleShow() {
            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;
                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.removeView(mView);
                }

                mView = null;
            }
        }
    }
实际上是一个aidl接口的实现类,保证与服务进程通信的。

这个类里面有个WindowManager示例,并且有个handler负责把我们自定义的view加入到WindowMagener中或者移除。

主要看它的成员变量mGrayvity,在toast初始化的时候得到了一个默认的值:

mTN.mGravity = context.getResources().getInteger(
                com.android.internal.R.integer.config_toastDefaultGravity);
    }
这个默认值是这样定义的:

<!-- Default Gravity setting for the system Toast view. Equivalent to: Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM -->
<integer name="config_toastDefaultGravity">0x00000051</integer>
即效果是居中,底部显示

在TN的handleShow方法中有这么一个判断:

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;
                }
这是横向或纵向的权重,权重为1表示充满这个屏幕,所以我们在定义toast的时候,使用setGrayvity设置属性:

sToast.setGravity(Gravity.BOTTOM|Gravity.FILL_HORIZONTAL, 0,300);
这样WindowManager就是横向全屏的了,里面的text就不会换行了。


另外一个重复显示的问题,只需要创建一个toast实例,当toast还在显示的时候,再次点击button时,不用去 重新创建实例,只要重新设置toast的时间,然后调用show就可以了。

 public static void showToast1(Activity act,CharSequence text, int length) {
	 // TODO Auto-generated method stub
		 if (sToast == null) {
			 LayoutInflater inflater = act.getLayoutInflater();
			 View layout = inflater.inflate(R.layout.toast_submit2, null);
			
			 TextView textView = (TextView) layout.findViewById(R.id.tv_toast);
			 textView.setText(text);
			
			 
			 sToast = new Toast(act.getApplicationContext());
			 sToast.setView(layout);
			 sToast.setGravity(Gravity.BOTTOM|Gravity.FILL_HORIZONTAL, 0,(int) act.getResources().getDimension(R.dimen.video_fwd_toast_postion_to_bottom));
		 }
		 sToast.setDuration(length);
		 sToast.show();
	 }
保证多次点击,只会有一个toast实例存在,show只是把这个实例重新加入到队列中,并且重新设置显示时间。

关于这个显示时间,如何自定义?并没有好的解决办法,如下所述:

 1:     private static final int LONG_DELAY = 3500; // 3.5 seconds
  2:     private static final int SHORT_DELAY = 2000; // 2 seconds    
  3:     private void scheduleTimeoutLocked(ToastRecord r, boolean immediate)
  4:     {
  5:         Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
  6:         long delay = immediate ? 0 : (r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY);
  7:         mHandler.removeCallbacksAndMessages(r);
  8:         mHandler.sendMessageDelayed(m, delay);
  9:     }

 

这一段源码位于\sources\Android-16\com\android\server包的NotificationManagerService类中,这里代码很明确的告诉我们,如果我们show一个Toast的时候,设置的时间为Toast.LENGTH_LONG,那么就按Toast.LENGTH_LONG来显示,如果设置了其他值则全部按Toast.LENGTH_SHORT来显示。而LONG_DELAY是3.5秒,SHORT_DELAY是2秒,也就是说一个Toast最长的显示时间为3.5秒。


如果我们要长时间的显示toast,可以在超时时间到来之前再一次调用:

 sToast.setDuration(length);
		 sToast.show();
需要显示多久就不停的计时调用多少次。似乎也只能这样了。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
你可以通过以下步骤来实现自定义 Toast 并支持设置消失时间: 1. 在 Vue 项目中创建一个自定义Toast 组件,可以参考下面的代码: ```vue <template> <div class="toast" v-show="show" :class="type"> {{ message }} </div> </template> <script> export default { name: 'Toast', props: { message: String, type: { type: String, default: 'info', }, time: { type: Number, default: 3000, }, }, data() { return { show: false, }; }, watch: { message() { this.show = true; setTimeout(() => { this.show = false; }, this.time); }, }, }; </script> <style scoped> .toast { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 999; padding: 10px 20px; border-radius: 5px; color: #fff; font-size: 14px; text-align: center; transition: opacity 0.3s ease-in-out; } .toast.info { background-color: #2196f3; } .toast.success { background-color: #4caf50; } .toast.warning { background-color: #ff9800; } .toast.error { background-color: #f44336; } </style> ``` 2. 在需要使用的组件中引入自定义Toast 组件,并在该组件中定义一个方法来显示 Toast,例如: ```vue <template> <div> <button @click="showToast">显示 Toast</button> <Toast :message="toastMessage" :type="toastType" :time="toastTime" /> </div> </template> <script> import Toast from '@/components/Toast.vue'; export default { name: 'MyComponent', components: { Toast, }, data() { return { toastMessage: '', toastType: 'info', toastTime: 3000, }; }, methods: { showToast() { this.toastMessage = 'Hello, World!'; this.toastType = 'info'; this.toastTime = 3000; }, }, }; </script> ``` 在上面的代码中,我们定义了一个 `showToast` 方法来显示 Toast,该方法会设置 `toastMessage`、`toastType` 和 `toastTime` 三个属性的值,并将这些值传递给 Toast 组件进行显示。`toastTime` 属性用来指定 Toast 消失的时间,默认为 3000 毫秒。 当 `showToast` 方法被调用后,它会设置 `toastMessage`、`toastType` 和 `toastTime` 三个属性的值,并触发这些属性的 watch 函数。在 watch 函数中,我们设置 `show` 属性为 true,表示显示 Toast,同时使用 setTimeout 函数来在指定的时间后将 `show` 属性设置为 false,从而让 Toast 消失。 以上就是一个简单的自定义 Toast 的实现方式,你可以根据自己的需求进行修改和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值