本文实现了一个Floating App,实际上是一个service,通过Service创建一个Window,为了保证这个window能够放置在Activity窗口之上,设定它的窗口类型为TYPE_SYSTEM_ALERT。
为了保证能够在屏幕上正常拖动并且在点击时又能正确响应这个window中的子控件的事件,重写了这个window的布局。不多说了,上代码(不包含布局)。
Floating App的manifest文件
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="aaa.bbb.ccc"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="18" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<service android:name=".FloatingService" >
<intent-filter>
<action android:name="aaa.bbb.ccc.FloatingService" />
</intent-filter>
</service>
</application>
<uses-permission
android:name="android.permission.SYSTEM_ALERT_WINDOW" />
</manifest>
需要注意的是,需要这个permission。
android:name="android.permission.SYSTEM_ALERT_WINDOW"
如果需要其他应用启动这个FloatingService,发隐式的Intent就可以了。
FloatingService的代码
public class FloatingService extends Service {
private WindowManager mWindowManager;
private LayoutInflater mInflater;
private WindowManager.LayoutParams mLayoutParams;
private FloatingLayout mFloatingLayout;
@Override
public IBinder onBind(Intent arg0) {
// TODO Auto-generated method stub
return null;
}
@Override
public void onCreate() {
super.onCreate();
makeFloatingView();
}
@Override
public void onDestroy() {
mWindowManager.removeView(mFloatingKeyLayout);
super.onDestroy();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// TODO Auto-generated method stub
return super.onStartCommand(intent, flags, startId);
}
private void makeFloatingView() {
mWindowManager = (WindowManager) getApplicationContext().getSystemService("window");
mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mLayoutParams = new WindowManager.LayoutParams();
mLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
mLayoutParams.format = PixelFormat.RGBA_8888;
mLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
mLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
mLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
mLayoutParams.x = 50;
mLayoutParams.y = 50;
mFloatingLayout = (FloatingLayout)mInflater.inflate(R.layout.float_layout, null);
mWindowManager.addView(mFloatingKeyLayout, mLayoutParams);
}
}
重写的布局文件,这里继承了LinearLayout
public class FloatingLayout extends LinearLayout implements
View.OnClickListener {
public static final String TAG = "FloatingLayout";
private Context mContext;
private Button mBtnXxx;
private boolean mIsBeingDragged = false;
/**
* Position of the last motion event.
*/
private float mLastMotionX;
private float mLastMotionY;
private final int mTouchSlop = 5;
private WindowManager mWindowManager;
private WindowManager.LayoutParams mLayoutParams;
public FloatingKeyLayout(Context context) {
super(context);
mContext = context;
}
public FloatingKeyLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mContext = context;
}
public FloatingKeyLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//取得这个LinearLayout中的各个子控件并设定事件监听器
mBtnXxx = (Button) findViewById(R.id.xxx);
mBtnXxx.setOnClickListener(this);
......
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (mWindowManager == null) {
mWindowManager = (WindowManager) getContext().getSystemService(
Context.WINDOW_SERVICE);
mLayoutParams = (WindowManager.LayoutParams) getLayoutParams();
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
switch (ev.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
final float x = ev.getX();
final float y = ev.getY();
final int xDiff = (int) Math.abs(x - mLastMotionX);
final int yDiff = (int) Math.abs(y - mLastMotionY);
if (xDiff > mTouchSlop || yDiff > mTouchSlop) {
mIsBeingDragged = true;
}
break;
}
case MotionEvent.ACTION_DOWN: {
final float x = ev.getRawX();
final float y = ev.getRawY();
mLastMotionX = x;
mLastMotionY = y;
mIsBeingDragged = false;
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
/* Release the drag */
mIsBeingDragged = false;
break;
}
return mIsBeingDragged;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
final float x = event.getRawX();
final float y = event.getRawY();
mLayoutParams.x = (int) (x - mLastMotionX + mLayoutParams.x);
mLayoutParams.y = (int) (y - mLastMotionY + mLayoutParams.y);
mWindowManager.updateViewLayout(this, mLayoutParams);
mLastMotionX = x;
mLastMotionY = y;
break;
}
case MotionEvent.ACTION_DOWN: {
final float x = event.getX();
final float y = event.getY();
mLastMotionX = x;
mLastMotionY = y;
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mIsBeingDragged = false;
break;
}
return true;
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.xxx: {
//do something
break;
}
......
}
}
}
这里主要处理了拖动事件的拦截,对于Touch事件的传递机制不太清楚的,可以参考文章 Android Touch事件在View层级结构中的传递机制
注意:这个计算位置的时候使用的是event.getRawX(),而不是getX(),这里如果使用getX出现了抖动的现象。
它们的区别如下:
getX()是表示Widget相对于自身左上角的x坐标
而getRawX()是表示相对于屏幕左上角的x坐标值
(注意:这个屏幕左上角是手机屏幕左上角,不管activity是否有titleBar或是否全屏幕),getY(),getRawY()一样的道理。