转载请注明文章出处:
1、Toast布局定制
Toast,相信每个App基本上都会用到,头疼的是有时候系统的风格不符合我们App的风格,那就需要自定义,其实Toast的定制是最简单的。根据我下面的步骤相信你很快也就学会Toast任何效果定制了。
首先第一步,在我们的程序中联想写出Toast.makeText(context, text, duration),按住Ctrl点击makeText进去观看Toast的源码如下:
/**
* 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) {
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;
}
是不是很神奇呢,原来是系统帮我们封装好了,让我们使用起来才那么简便,那么接下来就是复制这段系统的源码,我们新建一个类放进去。解决一下红色错误:a、删掉@
Duration 和导入它的包;b、接着可以看到它加载的布局是android系统的:把它换成我们自己定义的布局Id和控件Id就可以了;
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
c、那下面两句代码又是什么错误呢,可以复制mNextView 或者mDuration去Toast源码那边查找,可以发现,原来是权限问题,那怎么办呢?
result.mNextView = v;<pre name="code" class="java">result.mDuration = duration;
系统源码我们又改不了,Google也是很强大的哈,他提供了一些set(),get()方法可以获得和设置系统参数。如下:
/**
* Set the view to show.
* @see #getView
*/
public void setView(View view) {
mNextView = view;
}
/**
* Return the view.
* @see #setView
*/
public View getView() {
return mNextView;
}
当然mDuration也是有提供的,于是我们把那两句报错的代码换成:
result.setView(v);;
result.setDuration(duration);
这样,Toast的定制我们就完成了,下面附上完整代码:(自定义布局custom_toast很简单,随便定制哈,这就不贴源码了哈)
<pre name="code" class="html">public class CustomToast {
public static Toast makeText(Context context, CharSequence text, int duration) {
Toast result = new Toast(context);
LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(R.layout.custom_toast, null);
TextView tv = (TextView)v.findViewById(R.id.tv_toast);
tv.setText(text);
result.setView(v);;
result.setDuration(duration);
return result;
}
}
现在就只要在需要它的地方调用它就ok了,是不是巨简单啊。
CustomToast.makeText(this, "请按照顺序处理,每次只能处理一项",Toast.LENGTH_SHORT).show();
2、Toast解决点击没有立即响应bug
我们知道,当点击两个Toast时,Toast还在不断的显示之前那个Toast,直到把我们点过的全部显示一遍,那么我们不得不猜测这里有一个Toast队列,每当我们makeText的时候,系统就会往这个队列当中添加一个Toast,然后再不断从队列中取出一个一个的Toast显示,那么真实情况是不是这样呢?看看源码就知道了。 /**
* Show the view for the specified duration.
*/
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
这是show()里面的源码,这个show方法也不长,先是获得一个服务,然后拿到一个TN对象,再把Toast的视图交给这个TN的实例,最后调用服务的一个队列方法,把这个TN的实例扔到这个队列中去,看到队列两个字我们就应该明白了为什么会出现上面第一幅图的情况,原来我们每点击一次按钮,就会往这个队列中放一个Toast,当我们点击很多次之后不再点击了,但是队列中还是有很多Toast,这时系统就会把这些还没有显示过的Toast一个一个的读出来显示,这就是我们在第一幅图中看到的现象。那么我们循序渐进,一步一步的来分析这里的情况,看看怎么从根本上解决这个问题。先来看一下这里的这个getService()方法。
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
这里我们重点看一下这个方法的注释,意思是说下面这段代码,获得的是一个与系统通知有关的服务,这个服务控制着整个系统的通知正常有序的进行(大概就是这个意思吧,原谅我英语是个渣渣)。拿到这个服务之后呢,下面就是TN这个类了,我们先来看看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();
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;
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;
}
}
}
这个类虽然有点长,但是我们不用怕,一点一点来解剖,首先TN这个类继承自ITransientNotification.Stub,这个一看就是个AIDL,这里有两个方法添加了@override注解,看来这个接口里边有两个方法需要我们实现。一个是show一个是hide,这里用到了Handler,我们知道Handler内部也有排队机制,这里的show和hide方法主要是调用了两个线程mShow和mHide,而这两个线程最终调用的是handleShow和handleHide方法,先看这个handleShow方法,首先布局文件肯定得有,然后这里拿到了一个WindowManager,然后就是给mView设置各种布局参数,最后这一行代码非常重要mWM.addView(mView, mParams);看到这里恍然大悟,原来是Toast的视图是通过WindowManager的addView来加载的。再看这个handleHide方法,就是把mView从WindowManager中移除。现在我们再回过头来看TN的构造方法,在构造方法中就是对WindowManager的初始化。
这下我们应该有个大致的脉络了,当我们调用Toast的show方法时,并不会直接去显示它,而是先new一个TN变量,将这个TN对象的实例添加到队列中,至于Toast的显示与隐藏,则是通过TN来调控的,比如Toast的cancel方法,我们来看看这个方法的源码:
/**
* 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() {
mTN.hide();
try {
getService().cancelToast(mContext.getPackageName(), mTN);
} catch (RemoteException e) {
// Empty
}
}
果然是调用了TN的hide方法。
好了,分析了这么多,下面我们该说说正事了,
就是我们该怎么样随心所欲的控制Toast的显示时间?
看了上面的分析我想大家心中应该已经清楚了要怎么实现?就是让我们的队列中时时刻刻只有一个Toast,这样就不会点击完成很久之后Toast还在那里悠哉游哉的显示,下面是具体实现代码:
好了,分析了这么多,下面我们该说说正事了,
就是我们该怎么样随心所欲的控制Toast的显示时间?
看了上面的分析我想大家心中应该已经清楚了要怎么实现?就是让我们的队列中时时刻刻只有一个Toast,这样就不会点击完成很久之后Toast还在那里悠哉游哉的显示,下面是具体实现代码:
public class ToastUtil {
private static Toast toast;
public static void showTextLong(Context context, String text) {
if (toast == null) {
toast = Toast.makeText(context, text, Toast.LENGTH_LONG);
} else {
toast.setText(text);
toast.setDuration(Toast.LENGTH_LONG);
}
toast.show();
}
public static void showTextShort(Context context, String text) {
if (toast == null) {
toast = Toast.makeText(context, text, Toast.LENGTH_SHORT);
} else {
toast.setText(text);
toast.setDuration(Toast.LENGTH_SHORT);
}
toast.show();
}
public static void cancelToast() {
if (toast != null) {
toast.cancel();
}
}
}
我们只要一个Toast实例,当Toast不为空的时候我们只是重新设置它的显示文本和时间,调用方法如下:
ToastUtil.showTextLong(this, "111");
这样就能解决Toast点击完一个一定要等待其显示完才能显示第二个的bug,那么如果要自己让它显示多少秒,那又要怎么办呢?让我们接着分析哈。