Android懸浮窗使用小結
阿新 • • 發佈:2019-01-11
Android的視窗體系中,WindowManager佔有非常重要的地位,它封裝了新增、移除、更新視窗的方法,它是Activity、View的更加底層的管理類,使用WindowManager的其中一個例子就是製作懸浮窗或懸浮球之類的懸浮元件,這種懸浮元件不依賴某個Activity,它可以在任何介面顯示(只要你願意)。
這篇文章將對如何使用懸浮球做簡單總結,即使在android6.0下(android6.0使用動態許可權管理),它也可以正常工作。1.使用ButterKnife
ButterKnife可以方便的獲取到xml中定義的view的例項,比findViewById方便多了,使用ButterKnife非常簡單,可以總結為3步吧:
- 新增對應依賴
compile 'com.jakewharton:butterknife:7.0.0'
- 宣告id與對應的View
<pre style="font-family: 宋體; font-size: 9pt; background-color: rgb(255, 255, 255);"><pre name="code" class="java"> @Bind(R.id.start) Button start; @Bind(R.id.stop) Button stop; @Bind(R.id.bind) Button bind; @Bind(R.id.unbind) Button unbind;
- 獲取View
在onCreate中呼叫它就可以。通過這句呼叫,start,stop,bind,unbind幾個Button都被例項化了。ButterKnife.bind(this);
2.使用Service
我希望懸浮窗是在Service中被顯示出來的,並且它的管理也在Service中實現。Service有兩種啟動方式,對應了不同的用途,使用bindService方式啟動的Service可以過得一個Ibinder的例項,使用這個例項可以操作Service的所有Public方法,一般用於Activity和Service互動比較頻繁的場合下,所以我們這裡使用startService啟動Service比較合理。 分析清楚以後,開始實現程式碼:2.1Service
public class FlowWindowService extends Service {
private final String TAG = "FlowWindowService";
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG,"onCreate");
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG,"onBind");
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG,"onStartCommand");
showFlowWindow();
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG,"onDestroy");
}
public void showFlowWindow(){
Log.v(TAG,"showFlowWindow");
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
Button button = new Button(getApplicationContext());
button.setText("flow");
button.setBackgroundColor(Color.RED);
button.setWidth(100);
button.setHeight(100);
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.type = WindowManager.LayoutParams.TYPE_PHONE;
params.format = PixelFormat.RGBA_8888;
params.gravity = Gravity.LEFT | Gravity.TOP;
params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
params.width = 100;
params.height = 100;
params.x = 300;
params.y = 300;
windowManager.addView(button, params);
}
}
這裡一次性把整個Service都貼出來了。這段程式碼在Service的onStartCommand方法中建立了懸浮窗,建立懸浮窗非常簡單,使用WindowManager的addView新增一個View就好了,在Android6.0之前,是這樣的,Android6.0使用動態許可權,會有點不同,這裡先使用21版本的sdk避免android6.0動態許可權的問題,一切測試OK了再使用Android6.0的動態許可權獲取響應許可權。 懸浮窗的引數設定中,type是個比較重要的引數,它決定了你懸浮窗的優先順序 服務不要忘記在Manifest中宣告:
<service android:name=".FlowWindowService" />
2.2Activity中啟動Service
Activity非常簡單,它裡面只有四個Button:<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.konka.flowwindowtest.MainActivity">
<Button
android:id="@+id/start"
android:text="start"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/stop"
android:text="stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/bind"
android:text="bind"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/unbind"
android:text="unbind"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
MainActivity中使用ButterKnife例項化四個Button,並設定觸控事件監聽器:
public class MainActivity extends AppCompatActivity {
@Bind(R.id.start) Button start;
@Bind(R.id.stop) Button stop;
@Bind(R.id.bind) Button bind;
@Bind(R.id.unbind) Button unbind;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
start.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startService(new Intent(MainActivity.this,FlowWindowService.class));
}
});
stop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
stopService(new Intent(MainActivity.this,FlowWindowService.class));
}
});
bind.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
bindService(new Intent(MainActivity.this,FlowWindowService.class),connectionService, Context.BIND_AUTO_CREATE);
}
});
unbind.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
unbindService(connectionService);
}
});
}
ServiceConnection connectionService = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
bind和unbind按鈕知識我測試bindService和startService之間的差別用的,不用理會。最後宣告一個許可權:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
至此,介面如下:
點選start,啟動服務:
懸浮窗出現。退出應用後它還在:
退出應用後懸浮窗還在是因為我們沒有在Service退出的時候移除懸浮窗,在Service的onDestroy中移除即可:
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG,"onDestroy");
windowManager.removeView(button);
}
這意味著之前的程式碼需要略作修改,button和windowManager都必須是類中定義的,而不是方法中定義的。這是Android6.0之前的步驟,Android6.0這麼弄就不可以了,原因是Android6.0使用動態許可權策略。
2.3動態申請許可權
不過也很簡單,程式碼如下: private static final int REQUEST_PERMISSION_CODE = 1;
private void requestCameraPermission() {
requestPermissions(new String[]{Manifest.permission.SYSTEM_ALERT_WINDOW}, REQUEST_PERMISSION_CAMERA_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_PERMISSION_CODE) {
int grantResult = grantResults[0];
boolean granted = grantResult == PackageManager.PERMISSION_GRANTED;
Log.i(TAG, "onRequestPermissionsResult granted=" + granted);
}
}
使用requestPermisson方法申請SYSTEM_ALERT_WINDOW許可權,REQUEST_PERMISSON_CODE是一個整數,用來表示這次請求,它的值隨意。requestPermissions會導致onRequestPermissionsResult方法被回撥,在這個方法中我們就可以知道我們是不是申請到了許可權。最後,在MainActivity的onCreate方法中申請許可權即可。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
requestPermission();
如果許可權申請失敗,很有可能是應用程式的許可權太低,嘗試一下用系統簽名檔案給它簽名,然後就OK了。另外,在Android的模擬器上是可以直接申請許可權成功的。
2.4事件監聽
我們簡單給它新增單擊事件處理,每次點選切換一下它的背景色: button.setOnClickListener(new View.OnClickListener() {
int count = 0;
@Override
public void onClick(View v) {
if((count++)%2==0){
button.setBackgroundColor(Color.GREEN);
}else {
button.setBackgroundColor(Color.RED);
}
}
});
3 移動懸浮框
移動懸浮框也很簡單,使用windowManager的updateViewLayout即可,下面的程式碼製作了一個往復移動的懸浮框: Handler myHandler = new Handler();
Runnable runnable = new Runnable() {
boolean direct = true;
@Override
public void run() {
Log.v(TAG,"params.x: "+params.x);
if(direct){
params.x+=10;
if(params.x>800){
direct = false;
}
}else {
params.x-=10;
if(params.x<100){
direct = true;
}
}
windowManager.updateViewLayout(button,params);
if(!button.isAttachedToWindow()){
Log.v(TAG,"not attach to window");
}else{
myHandler.postDelayed(this,50);
}
}
};
public void moveFlowButton(){
Log.v(TAG,"moveFlowButton");
myHandler.postDelayed(runnable,500);
}
使用Handler提供定時器,效果如下:
4.解決not attached to window manager
這個程式執行的時候,點選stop按鈕停止服務,會導致上面的問題,完整的log如下: Process: com.konka.flowwindowtest, PID: 19478
java.lang.IllegalArgumentException: View=android.widget.Button{2553221 VFED..C.. ......I. 0,0-200,200} not attached to window manager
at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:456)
at android.view.WindowManagerGlobal.updateViewLayout(WindowManagerGlobal.java:368)
at android.view.WindowManagerImpl.updateViewLayout(WindowManagerImpl.java:99)
at com.konka.flowwindowtest.FlowWindowService$1.run(FlowWindowService.java:89)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:5969)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:801)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:691)
這個問題應該是Service已經銷燬了,但是button還想要更新位置造成的,所以應該在銷燬Service前先取消handler更新button的動畫。
首先定義一個執行緒退出標誌:
Boolean destoryStatus = false;
在onDestory中是它為true即可:
synchronized (this){
destoryStatus = true;
}
run方法如下:
Runnable runnable = new Runnable() {
boolean direct = true;
@Override
public void run() {
if(!destoryStatus){
Log.v(TAG,"params.x: "+params.x);
if(direct){
params.x+=10;
if(params.x>800){
direct = false;
}
}else {
params.x-=10;
if(params.x<100){
direct = true;
}
}
windowManager.updateViewLayout(button,params);
myHandler.postDelayed(this,50);
}
}
};
這樣就不會有這個問題了。