您的位置:首页 > 其它

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

2016-06-08 16:12 549 查看
之前在自定义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();需要显示多久就不停的计时调用多少次。似乎也只能这样了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: