背景:
在使用android手机时候,大家壁纸可能一般都是使用的静态壁纸,静态壁纸一般就是设置一张静态,这种静态壁纸因为是固定的一张图片,所以对壁纸触摸交互这块需求比较少,但是如果设置的是一个动态壁纸,那么这个触摸交互需求就会大大增加。具体可以看下面的一个整体旋转的动态壁纸触摸场景:
明显可以看到这里的有一个正方体一直在旋转着,在鼠标触摸桌面时候,不仅仅桌面有响应事件,也发现Wallpaper也会绘制一个触摸点为圆心的圆,那么下面就来剖析一下Wallpaper壁纸窗口为啥可以实现触摸事件的接收原理,下面是桌面和壁纸窗口图层的位置,桌面肯定是位于壁纸上面的。
学习了马哥input课程就知道,正常逻辑的话上面有图层覆盖而且可以接受事件的话,那么底部的窗口肯定无法接受事件,但是上面看到动态壁纸情况下,明显可以看到桌面响应了触摸事件,壁纸也有响应,这个事件透传我们下一篇来分析input汇总原理,本节先来剖析上层WallpaperService的事件接受。
剖析动态壁纸事件接收代码
动态壁纸要接收触摸事件,还是需要一些额外设置的,具体的设置方法如下:
@Override
public void onCreate(SurfaceHolder surfaceHolder) {
super.onCreate(surfaceHolder);
// By default we don't get touch events, so enable them.
setTouchEventsEnabled(true);
}
一般动态壁纸要继承WallpaperService时候会重写onCreate,在onCreate中需要调用 setTouchEventsEnabled(true)让壁纸服务可以接受对应的触摸事件,如果设置为true,默认是不可以收到触摸事件。
上面设置完成后就可以让系统把触摸事件传递到WallpaperSerivce了,具体哪个方法接收呢?
这里其实WallpaperSericce的内部类Engine也有个onTouchEvent方法,它可以实现对触摸事件的接受
一般会重写这个方法进行相关的事件处理,比如上面正方体动态壁纸的处理如下:
/*
* Store the position of the touch event so we can use it for drawing later
*/
@Override
public void onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
mTouchX = event.getX();
mTouchY = event.getY();
} else {
mTouchX = -1;
mTouchY = -1;
}
super.onTouchEvent(event);
}
这里的处理只是记录了一下坐标,记录坐标目的是为了在正方体绘制时候用这个坐标来绘制一个圆:
/*
* Draw one frame of the animation. This method gets called repeatedly
* by posting a delayed Runnable. You can do any drawing you want in
* here. This example draws a wireframe cube.
*/
void drawFrame() {
final SurfaceHolder holder = getSurfaceHolder();
Canvas c = null;
try {
c = holder.lockCanvas();
if (c != null) {
// draw something
drawCube(c);
drawTouchPoint(c);//绘制触摸点的圆
}
} finally {
if (c != null) holder.unlockCanvasAndPost(c);
}
// Reschedule the next redraw
mHandler.removeCallbacks(mDrawCube);
if (mVisible) {
mHandler.postDelayed(mDrawCube, 1000 / 25);
}
}
上面就是靠drawTouchPoint方法来绘制触摸点的圆:
/*
* Draw a circle around the current touch point, if any.
*/
void drawTouchPoint(Canvas c) {
if (mTouchX >=0 && mTouchY >= 0) {
c.drawCircle(mTouchX, mTouchY, 80, mPaint);
}
}
整体使用也是非常简单,那么下面来看看这块WallpaperSerice可以接受触摸事件的一个初步原理。
WallpaperService触摸事件接收原理
先从 setTouchEventsEnabled(true)作为切入点开始剖析
/**
* Control whether this wallpaper will receive raw touch events
* from the window manager as the user interacts with the window
* that is currently displaying the wallpaper. By default they
* are turned off. If enabled, the events will be received in
* {@link #onTouchEvent(MotionEvent)}.
*/
public void setTouchEventsEnabled(boolean enabled) {
mWindowFlags = enabled
? (mWindowFlags&~WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
: (mWindowFlags|WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
if (mCreated) {
updateSurface(false, false, false);
}
}
可以看到这里主要是对mWindowFlags进行了设置,如果enable就是不进行设置FLAG_NOT_TOUCHABLE,可以通过dump确认。
dumpsys window windows
默认的静态壁纸,不可以触摸
自定义动态壁纸,可以触摸,就没有flag为NOT_TOUCHABLE
那么setTouchEventsEnabled(true)后,事件又是怎么到传递到WallpaperService的呢?
可以看看updateSurface方法,在窗口添加时候创建好对应的InputChannel
所以上面就可以看得出来无论是否WallpaperService是否enable Touch,都会创建好对应的InputChannel即在InputDispatcher中都会有对应的InputWindow,只不过这个时候窗口InputWindow因为设置了NOT_TOUCHABLE导致不会进行派发到这个窗口。
可以通过dumpsys input来看看静态壁纸情况:
dumpsys input
再看看事件的怎么到的WallpaperSerice的onTouchEvent
先看看WallpaperInputEventReceiver接收
frameworks/base/core/java/android/service/wallpaper/WallpaperService.java
final class WallpaperInputEventReceiver extends InputEventReceiver {
public WallpaperInputEventReceiver(InputChannel inputChannel, Looper looper) {
super(inputChannel, looper);
}
@Override
public void onInputEvent(InputEvent event) {
boolean handled = false;
try {
if (event instanceof MotionEvent
&& (event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
MotionEvent dup = MotionEvent.obtainNoHistory((MotionEvent)event);
dispatchPointer(dup);//这里会调用dispatchPointer派发
handled = true;
}
} finally {
finishInputEvent(event, handled);
}
}
}
再看看dispatchPointer
private void dispatchPointer(MotionEvent event) {
if (event.isTouchEvent()) {
synchronized (mLock) {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
mPendingMove = event;
} else {
mPendingMove = null;
}
}
Message msg = mCaller.obtainMessageO(MSG_TOUCH_EVENT, event);//主要是发送了msg
mCaller.sendMessage(msg);
} else {
event.recycle();
}
}
再看看MSG_TOUCH_EVENT的消息处理:
case MSG_TOUCH_EVENT: {
boolean skip = false;
MotionEvent ev = (MotionEvent)message.obj;
if (ev.getAction() == MotionEvent.ACTION_MOVE) {
synchronized (mEngine.mLock) {
if (mEngine.mPendingMove == ev) {
mEngine.mPendingMove = null;
} else {
// this is not the motion event we are looking for....
skip = true;
}
}
}
if (!skip) {
if (DEBUG) Log.v(TAG, "Delivering touch event: " + ev);
mEngine.onTouchEvent(ev);//调用到mEngine的onTouchEvent
}
ev.recycle();
上面就展示了,最后事件到了WallpaperSerice内部类Engine的onTouchEvent方法。
具体事件是如何可以穿透壁纸上面窗口图层Launcher的,下一篇文章再给大家分析。
壁纸触摸事件总结:
1、壁纸窗口本身在创建时候就已经有创建好对应的InputChannel,而且也会和正常窗口一样会有InputWindow在InputDispatcher,而且位置一般处于最底层
2、要让壁纸可以接收事件,需要调用 setTouchEventsEnabled(true),主要是把Window不进行设置NOT_TOUCHABLE的flag。
3、事件在InputDispatcher派发时候,可以透过Launcher传递到Wallpaper的窗口,但如果有NOT_TOUCHABLE的flag那么就不会传递,具体如何可以透传下一篇分析。
更多framework实战开发干货,请关注下面“千里马学框架”