1. 程式人生 > >android 應用內懸浮框,並在指定頁面顯示

android 應用內懸浮框,並在指定頁面顯示

一、實現懸浮

懸浮框基本的實現方式有兩種:
1、
一個頁面內,可以用FrameLayout 或者RelativeLayout。FrameLayout 中view是在左上角堆疊的,也就是說是z-order的,所以可以頁面的基佈局是FrameLayout,然後在上面放一個view,並且更新view的translationX ,translationY來改變位置。
RelativeLayout 可以用alignParent屬性確定在父檢視的位置。不過位置不可變。
如果這種方式顯示全應用懸浮,那麼就要每個頁面都加,顯然有點工作量。
2、
windowmanager 新增檢視。這種方式比較靈活,不同activity都可以懸浮,甚至可以作為系統圖層,放在應用之上。

這裡使用第二種方式

先上程式碼

package com.android.aat;

import android.content.Context;
import android.graphics.PixelFormat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import
android.widget.ImageView; import android.widget.Toast; /** * Created by yueshaojun on 16/6/22. */ public class FloatView extends ImageView implements View.OnClickListener { private WindowManager wm; private WindowManager.LayoutParams wlp; private float x; private float y; private
float newX; private float newY; private boolean isRelease; private boolean isLongPress; private final long minTime = 300; private Context mContext; private boolean isAdded; Runnable run = new Runnable() { @Override public void run() { //短按 if(isRelease){ onClick(FloatView.this); return; } //長按 isLongPress = true; } }; public FloatView(Context context) { super(context); mContext = context; wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); wlp = new WindowManager.LayoutParams(); setOnClickListener(this); } public FloatView(Context context, AttributeSet attrs) { super(context, attrs); } public FloatView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean dispatchTouchEvent(MotionEvent event) { x=event.getRawX(); y=event.getRawY(); switch (event.getAction()){ case MotionEvent.ACTION_DOWN: Log.i("EVENT","DOWN"); isRelease = false; newX=event.getX(); newY=event.getY(); //300ms後檢測 如果沒有擡起手認為是長按 postDelayed(run,minTime); break; case MotionEvent.ACTION_MOVE: if(isLongPress){ Log.i("EVENT", "MOVE"); update(); } break; case MotionEvent.ACTION_UP: Log.i("EVENT","UP"); //標記已經擡起手 isRelease = true; if (isLongPress) { isLongPress = false; } break; } return true; } private void update() { if(wlp==null){ return; } //取view左上角座標 wlp.x = (int)(x-newX); wlp.y=(int)(y-newY); wm.updateViewLayout(this,wlp); } @Override public void onClick(View v) { Toast.makeText(mContext,"click!",Toast.LENGTH_LONG).show(); } public void show(){ //如果windowmanager已經新增過,則不處理 if(isAdded){ return; } isAdded = true; wlp.gravity= Gravity.LEFT| Gravity.TOP; wlp.width = 200; wlp.height = 200; wlp.x=0; wlp.y=0; wlp.format = PixelFormat.TRANSLUCENT; wlp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; wlp.type = WindowManager.LayoutParams.TYPE_TOAST; wm.addView(this,wlp); } public void dismiss(){ if(isAdded) { isAdded = false; wm.removeView(this); } } }

核心思想就是在windowmanager中新增view、移除view,監聽觸控事件更新view。windowmanager具體使用這裡不詳細講。這裡只簡單的繼承了imageView,當然可以通過繼承如linearLayout等佈局,新增自己定義的view上去。show的時候新增view,dismiss的時候移除view。

二、應用內懸浮

很尷尬的是,上面的方法顯示出來的懸浮框,在應用按了home鍵或者退出到桌面的時候還在。但是很多場景只想它在自己的應用裡懸浮,甚至只在指定的頁面懸浮。

方法網上有不少方法可以參考,我是這麼做的:
用一個helper維護FloatView 的例項,檢測當前activity是不是位於前臺,如果位於前臺,windowmanager新增檢視,位於後臺移除檢視;同樣的,檢查當前activity是否在過濾列表,在就不顯示。這樣就可以達到在應用內的指定頁面懸浮。
程式碼如下:

public class FloatViewHelper {
    private static final String TAG = "FloatViewHelper";
    private static FloatView mFloatView;
    private static List<String> notShowList = new ArrayList();
    static {
         //新增不需要顯示的頁面
        notShowList.add("MainActivity");
    }

    public static void showFloatView(final Context context){

        if(!CommonUtils.isAppOnForeground(context)||CommonUtils.isTargetRunningForeground(context,notShowList)){
            return;
        }
        if(mFloatView == null){
            mFloatView = new FloatView(context.getApplicationContext());
        }
        mFloatView.show();
    }

    public static void removeFloatView(Context context){
        if(CommonUtils.isAppOnForeground(context)&&CommonUtils.isTargetRunningForeground(context,notShowList)){
            return;
        }
        if(mFloatView ==null||mFloatView.getWindowToken()==null){
            return;
        }

        mFloatView.dismiss();
    }
    public static void addFilterActivities(List<String> activityNames){
        notShowList.addAll(activityNames);
    }
}

isAppOnForeground 方法貼出來:

/**
     * 程式是否在前臺執行
     *
     * @return
     */
    public static boolean isAppOnForeground(Context context) {
        // Returns a list of application processes that are running on the
        // device
        ActivityManager activityManager = (ActivityManager) context
                .getApplicationContext()
                .getSystemService(Context.ACTIVITY_SERVICE);
        String packageName = context.getApplicationContext().getPackageName();
        List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager
                .getRunningAppProcesses();
        if (appProcesses == null)
            return false;
        for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
            // The name of the process that this object is associated with.
            if (appProcess.processName.equals(packageName)
                    && appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
                return true;
            }
        }
        return false;

    }

檢查當前頁面是否在過濾列表:

public static boolean isTargetRunningForeground(Context context,List<String> targetActivityNames) {
        String topActivityName =  ((Activity)context).getClass().getSimpleName();
        if (!TextUtils.isEmpty(topActivityName) && targetActivityNames.contains(topActivityName)) {
            return true;
        }

        return false;
    }

然後在BaseActivity的onStart()中使用FloatViewHelper.showFloatView(this);onPause()中使用FloatViewHelper.removeFloatView(this);之後所有activity都繼承這個BaseActivity。

為什麼在onStart()和onPause()中使用

這個要說到activity的生命週期。正常情況下,從activity A跳轉到activity B,經歷的生命週期是:
A:onCreate()–>onStart()–> onResume()–>onPause()->B:onCreate()–>onStart()–>onResume()–>A :onStop()–>onDestory()。
那我們要實現的就是,前一個頁面跳轉到後一個頁面的時候,前一個頁面remove,後一個頁面show,所以從週期來講,應該在onResum()方法中show,在onPause()方法中remove。

到此就大功告成啦!

在android7.0以上(自測7.1.1),因為對視窗許可權做了限制,所以要做相容處理:
FloatView.java

if(Build.VERSION.SDK_INT > 24) {
            wlp.type = WindowManager.LayoutParams.TYPE_PHONE;
        }else {
            wlp.type = WindowManager.LayoutParams.TYPE_TOAST;
        }

然後在使用之前要在AndroidManifest.xml中新增android.permission.SYSTEM_ALERT_WINDOW 這個許可權,然後在使用之前,判斷有沒有許可權:

private static boolean hasPermission(Context context){
        if( Build.VERSION.SDK_INT > 24 ) {
            return Settings.canDrawOverlays(context);
        }
        return true;
    }

注意,試過checkPermission()等方法,發現並不起作用,要使用Settings.canDrawOverlays(context)判斷。