效果圖:(懸浮框可拖動)
在項目開發中有一個需求:彈出懸浮窗后,響應懸浮窗的事件再彈出對話框,但是對話框怎么也不顯示。也就是說在彈出懸浮框的同時,不能再彈出對話框,可能的原因:
1.懸浮框的焦點在最前面,把對話框擋住了,我們看不到。
2.浮動框限制了對話框的彈出。
解決:
彈出對話框的時候把懸浮框關掉,然后對話框處理完了,把對話框關掉,在重新開啟一個懸浮框,把需要的值傳進去。
就相關知識詳解:
當我們在手機上使用360安全衛士時,手機屏幕上時刻都會出現一個小浮動窗口,點擊該浮動窗口可跳轉到安全衛士的操作界面,而且該浮動窗口不受其他activity的覆蓋影響仍然可見(多米音樂也有相關的和主界面交互的懸浮窗口)。那么不受acitvity影響的懸浮窗口是怎么實現的呢?
竟然它能懸浮在手機桌面,且不受Activity界面的影響,說明該懸浮窗口是不隸屬於Activity界面的,也就是說,他是隸屬於啟動它的應用程序所在進程。如360App所在的應用進程,當殺掉它所在的應用進程時,它才會消失。
懸浮窗口的實現涉及到WindowManager(基於4.0源碼分析),它是一個接口,實現類有WindowManagerImpl,CompatModeWrapper(WindowManagerImpl的內部類),LocalWindowManager(Window的內部類),它們之間的關系如下圖的類圖:
WindowManagerImpl:
1.是WindowManager的實現類,windowmanager的大部分操作都在這里實現,但是並不會直接調用,而是作為LocalWindowManager和WindowManagerImpl.CompatModeWrapper的成員變量來使用。
2.在WindowManagerImpl中有3個數組View[],ViewRoot[],WindowManager.LayoutParams[],分別用來保存每個圖層的數據。
3.WindowManagerImpl最重要的作用就是用來管理View,LayoutParams, 以及ViewRoot這三者的對應關系。
LocalWindowManager:
在源碼的Activity類中,有一個重要的成員變量mWindow(它的實現類為PhoneWindow),同時也有一個成員變量mWindowManager(跟蹤源碼可知它是一個LocalWindowManager),而在PhoneWindow中同時也有和Activity相同名字的mWindowManager成員變量。而且Activity中的mWindowManager是通過Window類中的setWindowManager函數初始化獲取的。
所以,在Activity中的LocalWindowManager的生命周期是小於Activity的生命周期的,而且在ActivityThread每創建一個Activity時都有該Activity對應的一個屬於它的LocalWindowManager。
對LocalWindowManager的小結:
1.該類是Window的內部類,父類為CompatModeWrapper,同樣都是實現WindowManager接口。
2.每個Activity中都有一個mWindowManager成員變量,Window類中 也有相應的同名字的該成員變量。該變量是通過調用Window的setWindowManager方法初始化得到的,實際上是一個LocalWindowManger對象。
3.也就說,每生成的一個Activity里都會構造一個其相應LocalWindowManger來管理該Activity承載的圖層。(該對象可以通過Activity.getWindowManager或getWindow().getWindowManager獲取)
4.LocalWindowMangers 的生命周期小於Activity的生命周期,(因為mWindowManager是Window的成員變量,而mWindow又是Activity的成員變量),所以,如果我們在一個LocalwindowManager中手動添加了其他的圖層, 在Activity的finish執行之前, 應該先調用LocalwindowManager的removeView, 否則會拋出異常。
CompatModeWrapper:
該類就是實現懸浮窗口的重要類了。
跟蹤源碼可知:
1.CompatModeWrapper相當於是一個殼,而真正實現大部分功能的是它里面的成員變量mWindowManager(WindowManagerImpl類)。
2.該對象可以通過getApplication().getSystemService(Context.WINDOW_SERVICE)得到。(注:如果是通過activity.getSystemService(Context.WINDOW_SERVICE)得到的只是屬於Activity的LocalWindowManager)。
3.這個對象的創建是在每個進程開始的時候, 通過ContextImpl中的靜態代碼塊創建的, 它使用了單例模式, 保證每個application只有一個。
4.通過該類可以實現創建添加懸浮窗口,也就是說,在退出當前Activity時,通過該類創建的視圖還是可見的,它是屬於整個應用進程的視圖,存活在進程中,不受Activity的生命周期影響。
ok,在通過上面對WindowManager接口的實現類做一些簡要的介紹后,接下來就動手編寫實現懸浮窗口的App。既然我們知道可以通過getApplication().getSystemService(Context.WINDOW_SERVICE)得到CompatModeWrapper,然后實現應用添加懸浮窗口視圖。那么,具體的實現操作可以在Activity或者Service中(這兩者都是可以創建存活在應用進程中的android重要組件)實現。
下面的App程序代碼實現通過主Activity的啟動按鈕,啟動一個Service,然后在Service中創建添加懸浮窗口:
要獲取CompatModeWrapper,首先得在應用程序的AndroidManifest.xml文件中添加權限<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
MainActivity的代碼如下:
public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //獲取啟動按鈕 Button start = (Button)findViewById(R.id.start_id); //獲取移除按鈕 Button remove = (Button)findViewById(R.id.remove_id); //綁定監聽 start.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub Intent intent = new Intent(MainActivity.this, FxService.class); //啟動FxService startService(intent); finish(); } }); remove.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { //uninstallApp("com.phicomm.hu"); Intent intent = new Intent(MainActivity.this, FxService.class); //終止FxService stopService(intent); } }); } }
FxService的代碼如下:
package com.phicomm.hu; import android.app.Service; import android.content.Intent; import android.graphics.PixelFormat; import android.os.Handler; import android.os.IBinder; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.view.View.OnClickListener; import android.view.View.OnTouchListener; import android.view.WindowManager.LayoutParams; import android.widget.Button; import android.widget.LinearLayout; import android.widget.Toast; public class FxService extends Service { //定義浮動窗口布局 LinearLayout mFloatLayout; WindowManager.LayoutParams wmParams; //創建浮動窗口設置布局參數的對象 WindowManager mWindowManager; Button mFloatView; private static final String TAG = "FxService"; @Override public void onCreate() { // TODO Auto-generated method stub super.onCreate(); Log.i(TAG, "oncreat"); createFloatView(); } @Override public IBinder onBind(Intent intent) { // TODO Auto-generated method stub return null; } private void createFloatView() { wmParams = new WindowManager.LayoutParams(); //獲取的是WindowManagerImpl.CompatModeWrapper mWindowManager = (WindowManager)getApplication().getSystemService(getApplication().WINDOW_SERVICE); Log.i(TAG, "mWindowManager--->" + mWindowManager); //設置window type wmParams.type = LayoutParams.TYPE_PHONE; //設置圖片格式,效果為背景透明 wmParams.format = PixelFormat.RGBA_8888; //設置浮動窗口不可聚焦(實現操作除浮動窗口外的其他可見窗口的操作) wmParams.flags = LayoutParams.FLAG_NOT_FOCUSABLE; //調整懸浮窗顯示的停靠位置為左側置頂 wmParams.gravity = Gravity.LEFT | Gravity.TOP; // 以屏幕左上角為原點,設置x、y初始值,相對於gravity wmParams.x = 0; wmParams.y = 0; //設置懸浮窗口長寬數據 wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT; wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT; /*// 設置懸浮窗口長寬數據 wmParams.width = 200; wmParams.height = 80;*/ LayoutInflater inflater = LayoutInflater.from(getApplication()); //獲取浮動窗口視圖所在布局 mFloatLayout = (LinearLayout) inflater.inflate(R.layout.float_layout, null); //添加mFloatLayout mWindowManager.addView(mFloatLayout, wmParams); //浮動窗口按鈕 mFloatView = (Button)mFloatLayout.findViewById(R.id.float_id); mFloatLayout.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec .makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); Log.i(TAG, "Width/2--->" + mFloatView.getMeasuredWidth()/2); Log.i(TAG, "Height/2--->" + mFloatView.getMeasuredHeight()/2); //設置監聽浮動窗口的觸摸移動 mFloatView.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { // TODO Auto-generated method stub //getRawX是觸摸位置相對於屏幕的坐標,getX是相對於按鈕的坐標 wmParams.x = (int) event.getRawX() - mFloatView.getMeasuredWidth()/2; Log.i(TAG, "RawX" + event.getRawX()); Log.i(TAG, "X" + event.getX()); //減25為狀態欄的高度 wmParams.y = (int) event.getRawY() - mFloatView.getMeasuredHeight()/2 - 25; Log.i(TAG, "RawY" + event.getRawY()); Log.i(TAG, "Y" + event.getY()); //刷新 mWindowManager.updateViewLayout(mFloatLayout, wmParams); return false; //此處必須返回false,否則OnClickListener獲取不到監聽 } }); mFloatView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub Toast.makeText(FxService.this, "onClick", Toast.LENGTH_SHORT).show(); } }); } @Override public void onDestroy() { // TODO Auto-generated method stub super.onDestroy(); if(mFloatLayout != null) { //移除懸浮窗口 mWindowManager.removeView(mFloatLayout); } } }
懸浮窗口的布局文件為R.layout.float_layout,所以,如果我們想設計一個非常美觀的懸浮窗口,可以在該布局文件里編寫。當然,也可以使用自定義View來設計(哈哈,少年們,在此基礎上發揮想象吧)。
上面代碼的效果圖如下:左邊為啟動界面。點擊“啟動懸浮窗口”按鈕,會啟動后台service創建懸浮窗口,同時finish當前Activity,這樣一個懸浮窗口就創建出來了,該窗口可實現任意位置移動,且可點擊監聽創建Toast提示(當然,也可以啟動一個Activity)。若要移除已創建的窗口,可點擊“移除懸浮窗口按鈕”,或者強制禁止該應用進程。
同樣的,在一個Activity里繪制懸浮視圖,不過下面的代碼主要還是驗證區分LocalWindowManger和CompatModeWrapper添加的視圖。
LocalWindowManger可通過activity.getSystemService(Context.WINDOW_SERVICE)或getWindow().getWindowManager獲取。當我們通過LocalWindowManger添加視圖時,退出Activity,添加的視圖也會隨之消失。
驗證代碼如下:
package com.phicomm.hu; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.graphics.PixelFormat; import android.os.Bundle; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.view.View.OnClickListener; import android.view.View.OnTouchListener; import android.view.WindowManager.LayoutParams; import android.widget.Button; import android.widget.LinearLayout; public class FloatWindowTest extends Activity { /** Called when the activity is first created. */ private static final String TAG = "FloatWindowTest"; WindowManager mWindowManager; WindowManager.LayoutParams wmParams; LinearLayout mFloatLayout; Button mFloatView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //createFloatView(); setContentView(R.layout.main); Button start = (Button)findViewById(R.id.start); Button stop = (Button)findViewById(R.id.stop); start.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub createFloatView(); //finish(); //handle.post(r); } }); stop.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub if(mFloatLayout != null) { mWindowManager.removeView(mFloatLayout); finish(); } } }); } private void createFloatView() { //獲取LayoutParams對象 wmParams = new WindowManager.LayoutParams(); //獲取的是LocalWindowManager對象 mWindowManager = this.getWindowManager(); Log.i(TAG, "mWindowManager1--->" + this.getWindowManager()); //mWindowManager = getWindow().getWindowManager(); Log.i(TAG, "mWindowManager2--->" + getWindow().getWindowManager()); //獲取的是CompatModeWrapper對象 //mWindowManager = (WindowManager) getApplication().getSystemService(Context.WINDOW_SERVICE); Log.i(TAG, "mWindowManager3--->" + mWindowManager); wmParams.type = LayoutParams.TYPE_PHONE; wmParams.format = PixelFormat.RGBA_8888;; wmParams.flags = LayoutParams.FLAG_NOT_FOCUSABLE; wmParams.gravity = Gravity.LEFT | Gravity.TOP; wmParams.x = 0; wmParams.y = 0; wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT; wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT; LayoutInflater inflater = this.getLayoutInflater();//LayoutInflater.from(getApplication()); mFloatLayout = (LinearLayout) inflater.inflate(R.layout.float_layout, null); mWindowManager.addView(mFloatLayout, wmParams); //setContentView(R.layout.main); mFloatView = (Button)mFloatLayout.findViewById(R.id.float_id); Log.i(TAG, "mFloatView" + mFloatView); Log.i(TAG, "mFloatView--parent-->" + mFloatView.getParent()); Log.i(TAG, "mFloatView--parent--parent-->" + mFloatView.getParent().getParent()); //綁定觸摸移動監聽 mFloatView.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { // TODO Auto-generated method stub wmParams.x = (int)event.getRawX() - mFloatLayout.getWidth()/2; //25為狀態欄高度 wmParams.y = (int)event.getRawY() - mFloatLayout.getHeight()/2 - 40; mWindowManager.updateViewLayout(mFloatLayout, wmParams); return false; } }); //綁定點擊監聽 mFloatView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub Intent intent = new Intent(FloatWindowTest.this, ResultActivity.class); startActivity(intent); } }); } }
將上面的代碼相關注釋部分取消,然后運行代碼查看Log信息,那么就可以知道問題所在了(每一個Activity對應一個LocalWindowManger,每一個App對應一個CompatModeWrapper),所以要實現在App所在進程中運行的懸浮窗口,當然是得要獲取CompatModeWrapper,而不是LocalWindowManger。
本文相關的完整代碼下載鏈接:
http://pan.baidu.com/s/1sjHsWJ7 提取碼:xt4u