悬浮框基本的实现方式有两种:
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 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 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 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 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。
这个要说到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)判断。