android 应用内悬浮框,并在指定页面显示

  • Post author:
  • Post category:其他


一、实现悬浮

悬浮框基本的实现方式有两种:

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)判断。



版权声明:本文为y1962475006原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。