android中自定义Toast方法详解(一)
在进行android开发时,我们经常用到Toast来给用于进行提示,大部分情况下android中提供给我们的默认方法基本上能够解决问题。但问题是有些用户嫌android原生的Toast不好看,想定义属于他们自己风格的Toast。考虑到别具一格的Toast确实能给用于耳目一新的感觉,因此我们也有了仔细去分析它实现细节的理由哈。先来看看demo中显示的android原生的Toast风格。
public void startToast(View view){ Toast.makeText(MainActivity.this, "您点击了Button按钮", Toast.LENGTH_SHORT).show();}
为了一探究竟,我们进入makeText()方法中,我们来看看android中是怎样来实现该方法的:
//android官方的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;}
从代码中可以看出,其实就是用LayoutInflater系统方法导入了一个android中内置的布局文件,我们再来看看这个原生的布局文件:
//com.android.internal.R.layout.transient_notification布局文件<?xml version="1.0" encoding="utf-8"?>
com.android.internal.R.id.message就是这上面的TextView控件的ID。makeText方法中传入的text的参数赋值给了TextView。接着又把View赋值给了Toast的成员函数并返回了一个Toast类型的对象。说白了这里其实就是通过makeText方法构造出了一个Toast类型的对象,然后将android中内置的布局文件转成一个View对象以及显示的时间传给Toast的成员变量。
再来看看show()方法的实现原理。//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方法中我们看TN类。这个方法中又把前面makeText方法中传递过来的View类型的对象给了TN类中的成员变量mNextView.貌似这个方法里也就做了这件事了。所以接下来我们要做的就是要去看看这个TN类具体长啥样。
原有的TN类有点长,这里我们删除了不影响我们理解的部分代码。
//TN类 private static class TN extends ITransientNotification.Stub { 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; } 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(); } } 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构造函数中设置了Toast的一些属性FLAG_NOT_FOCUSABLE FLAG_NOT_TOUCHABLE等表明Toast没有交点 以及没有Touch事件。接下来在handleShow()方法中我们注意到mWM是一个WindowManager类型的窗口管理者,为了在手机屏幕上显示出Toast,只需要将我们传进来的mNextView加到mWM中就行了。所以就有了下面的
mWM.addView(mView, mParams); 这里第二个参数定义了我们Toast摆放的位置等等。哦,原来把我们的View对象加入到WindowManager类型的管理器中就行了啊。罗里吧嗦说了那么多,咱们赶紧来试试吧。
//activity_main.xml
//toast.view.xml<?xml version="1.0" encoding="utf-8"?>
//button按钮的点击事件 public void startToast(View view){ //Toast.makeText(MainActivity.this, "您点击了Button按钮", Toast.LENGTH_SHORT).show(); toastMessage("您点击了Button按钮"); } /** * 该方法用于显示我们的Toast * @param content:显示的具体内容 */ public void toastMessage(String content){ LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); view = inflater.inflate(R.layout.toast_view, null); TextView tv = (TextView) view.findViewById(R.id.tv_address); tv.setText(content); view.setBackgroundResource(R.drawable.green); wm = (WindowManager) getSystemService(WINDOW_SERVICE); WindowManager.LayoutParams params = new WindowManager.LayoutParams(); params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.format = PixelFormat.TRANSLUCENT; 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; wm.addView(view,params); }}
(很丑。。。。但是至少实现了我们想要的效果哈)
这里在布局文件中设置了一个Button按钮,并给它设置了一个点击事件。这里我们用toastMessage方法封装类似Toast.makeText().show()的功能。我们用LayoutInflate引入了我们自定义的布局文件R.layout.toast_view。在该文件中定义了我们想要显示的样式。将它加入到窗口管理器中就可以得到我们自定义的Toast啦。但是此时我们应该还来不及庆祝,应该大家将这个代码跑一遍就知道Toast是显示出来了,但是貌似是一直显示在那 除非我们把该应用杀死,他才会在手机上消失呢!
怎么解决这个问题呢?我们注意到我们在上面分析源码时handleHide()方法中有一个mWM.removeView(mView); 看了前面的mWM.addView()就应该知道这其实就是移除我们的Toast了。所以,这里我们可以设置在子线程中设置过了一定的时间就自动的移除它,当然具体的场景中显示的肯定不一样。比如在打电话显示号码归属地时,打电话时可以显示出来,等到电话挂断时就可以移除它了。我们这里用线程设置过了5秒就自动移除它。
package com.mobilesafe.yuwei.toast;import android.app.ActionBar;import android.graphics.PixelFormat;import android.os.Bundle;import android.os.Handler;import android.os.Message;import android.support.v7.app.ActionBarActivity;import android.view.LayoutInflater;import android.view.View;import android.view.WindowManager;import android.widget.TextView;public class MainActivity extends ActionBarActivity { private WindowManager wm = null; private View view = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ActionBar actionBar = getActionBar(); if (actionBar != null) { actionBar.hide(); } } public void startToast(View view) {// Toast.makeText(MainActivity.this, "您点击了Button按钮", Toast.LENGTH_SHORT).show(); toastMessage("您点击了Button按钮"); } /** * 该方法用于显示我们的Toast * @param content:显示的具体内容 */ public void toastMessage(String content) { LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); view = inflater.inflate(R.layout.toast_view, null); TextView tv = (TextView) view.findViewById(R.id.tv_address); tv.setText(content); view.setBackgroundResource(R.drawable.green); wm = (WindowManager) getSystemService(WINDOW_SERVICE); WindowManager.LayoutParams params = new WindowManager.LayoutParams(); params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.format = PixelFormat.TRANSLUCENT; 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; wm.addView(view, params); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(5000); Message msg = handler.obtainMessage(); msg.what = 1; handler.sendMessage(msg); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } Handler handler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); if (1 == msg.what) { wm.removeView(view); view = null; wm = null; } } };}
成功啦!这里要注意的是子线程是不能更新android中的UI控件的,所以需要定义一个handler来移除我们的Toast,当然,为了做的更逼真一点,大家可以自己设置Toast显示的时间哈,这个就比较简单了。
下篇文章中我们将一块学习可以自由移动的自定义Toast的实现原理
更多相关文章
- Android 文件管理方法
- android:布局参数,控件属性及各种xml的作用
- Android在网络中与JavaWeb的项目进行交互的方法
- Android加载Gif图片的一般方法:Movie实现
- Android布局案例之人人android九宫格
- Android、iPhone和Java三个平台一致的加密方法
- Android中布局的巧妙设计【android进化二十六】