平台
RK3288 + Android 7.1
切入点
在点击近期任务后, 长按任务会出现如下界面, 拖动任务到指定区域可以进入分屏模式(DOCK):
-
主窗体 com.android.systemui/.recents.RecentsActivity
-
布局 frameworks/base/packages/SystemUI/res/layout/recents.xml
-
核心View控件RecentView
|-- frameworks/base/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java -
触摸处理
|-- frameworks/base/packages/SystemUI/src/com/android/systemui/recents/views/RecentsViewTouchHandler.java -
长按任务后显示的字符
|-- frameworks/base/packages/SystemUI/res/values/strings.xml
<string name="recents_drag_hint_message" msgid="2649739267073203985">"在此处拖动即可使用分屏功能"</string>
- 字符使用
|-- frameworks/base/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
DockState(int dockSide, int createMode, int dockAreaAlpha, int hintTextAlpha,
@TextOrientation int hintTextOrientation, RectF touchArea, RectF dockArea,
RectF expandedTouchDockArea) {
this.dockSide = dockSide;
this.createMode = createMode;
this.viewState = new ViewState(dockAreaAlpha, hintTextAlpha, hintTextOrientation,
R.string.recents_drag_hint_message);
this.dockArea = dockArea;
this.touchArea = touchArea;
this.expandedTouchDockArea = expandedTouchDockArea;
}
- 应用列表
|-- frameworks/base/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
//长按后的放大率
static final float DRAG_SCALE_FACTOR = 1.05f;
//开始拖动
public final void onBusEvent(DragStartEvent event) {
// Ensure that the drag task is not animated
addIgnoreTask(event.task);
if (event.task.isFreeformTask()) {
// Animate to the front of the stack
mStackScroller.animateScroll(mLayoutAlgorithm.mInitialScrollP, null);
}
// Enlarge the dragged view slightly
float finalScale = event.taskView.getScaleX() * DRAG_SCALE_FACTOR;
mLayoutAlgorithm.getStackTransform(event.task, getScroller().getStackScroll(),
mTmpTransform, null);
mTmpTransform.scale = finalScale;
mTmpTransform.translationZ = mLayoutAlgorithm.mMaxTranslationZ + 1;
mTmpTransform.dimAlpha = 0f;
updateTaskViewToTransform(event.taskView, mTmpTransform,
new AnimationProps(DRAG_SCALE_DURATION, Interpolators.FAST_OUT_SLOW_IN));
}
-
应用列表触摸事件, 如滑动, 本章中并非关键作用
|-- frameworks/base/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java -
任务项长按处理, 开始任务的拖拽
|-- frameworks/base/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
@Override
public boolean onLongClick(View v) {
SystemServicesProxy ssp = Recents.getSystemServices();
boolean inBounds = false;
Rect clipBounds = new Rect(mViewBounds.mClipBounds);
if (!clipBounds.isEmpty()) {
// If we are clipping the view to the bounds, manually do the hit test.
clipBounds.scale(getScaleX());
inBounds = clipBounds.contains(mDownTouchPos.x, mDownTouchPos.y);
} else {
// Otherwise just make sure we're within the view's bounds.
inBounds = mDownTouchPos.x <= getWidth() && mDownTouchPos.y <= getHeight();
}
if (v == this && inBounds && !ssp.hasDockedTask()) {
// Start listening for drag events
setClipViewInStack(false);
mDownTouchPos.x += ((1f - getScaleX()) * getWidth()) / 2;
mDownTouchPos.y += ((1f - getScaleY()) * getHeight()) / 2;
EventBus.getDefault().register(this, RecentsActivity.EVENT_BUS_PRIORITY + 1);
//发送开始拖动事件到EventBus, TaskStackView接收到后, 会放大TaskView
EventBus.getDefault().send(new DragStartEvent(mTask, this, mDownTouchPos));
return true;
}
return false;
}
- RecentView接收并处理DragStartEvent
|-- frameworks/base/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
mTouchHandler = new RecentsViewTouchHandler(this);
public final void onBusEvent(DragStartEvent event) {
//显示拖放坞(Dock), 见上面的图片
updateVisibleDockRegions(mTouchHandler.getDockStatesForCurrentOrientation(),
true /* isDefaultDockState */, TaskStack.DockState.NONE.viewState.dockAreaAlpha,
TaskStack.DockState.NONE.viewState.hintTextAlpha,
true /* animateAlpha */, false /* animateBounds */);
// Temporarily hide the stack action button without changing visibility
if (mStackActionButton != null) {
mStackActionButton.animate()
.alpha(0f)
.setDuration(HIDE_STACK_ACTION_BUTTON_DURATION)
.setInterpolator(Interpolators.ALPHA_OUT)
.start();
}
}
- 后续拖动及触摸释放, 若释放前处理有效的分屏区域, 则启动进入分屏模式, 如上图
|-- frameworks/base/packages/SystemUI/src/com/android/systemui/recents/views/RecentsViewTouchHandler.java
public final void onBusEvent(DragStartEvent event) {
SystemServicesProxy ssp = Recents.getSystemServices();
//停止父控件拦截输入事件
mRv.getParent().requestDisallowInterceptTouchEvent(true);
//后续触摸由此类处理
mDragRequested = true;
// We defer starting the actual drag handling until the user moves past the drag slop
mIsDragging = false;
mDragTask = event.task;
mTaskView = event.taskView;
mDropTargets.clear();
int[] recentsViewLocation = new int[2];
mRv.getLocationInWindow(recentsViewLocation);
mTaskViewOffset.set(mTaskView.getLeft() - recentsViewLocation[0] + event.tlOffset.x,
mTaskView.getTop() - recentsViewLocation[1] + event.tlOffset.y);
float x = mDownPos.x - mTaskViewOffset.x;
float y = mDownPos.y - mTaskViewOffset.y;
mTaskView.setTranslationX(x);
mTaskView.setTranslationY(y);
mVisibleDockStates.clear();
if (ActivityManager.supportsMultiWindow() && !ssp.hasDockedTask()
&& mDividerSnapAlgorithm.isSplitScreenFeasible()) {
Recents.logDockAttempt(mRv.getContext(), event.task.getTopComponent(),
event.task.resizeMode);
if (!event.task.isDockable) {
EventBus.getDefault().send(new ShowIncompatibleAppOverlayEvent());
} else {
// Add the dock state drop targets (these take priority)
TaskStack.DockState[] dockStates = getDockStatesForCurrentOrientation();
for (TaskStack.DockState dockState : dockStates) {
registerDropTargetForCurrentDrag(dockState);
dockState.update(mRv.getContext());
mVisibleDockStates.add(dockState);
}
}
}
// 初始化了DropTarget, 要切换到分屏, 会先显示放置位置的坞或站点
// Request other drop targets to register themselves
EventBus.getDefault().send(new DragStartInitializeDropTargetsEvent(event.task,
event.taskView, this));
}
private void handleTouchEvent(MotionEvent ev) {
int action = ev.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownPos.set((int) ev.getX(), (int) ev.getY());
break;
case MotionEvent.ACTION_MOVE: {
float evX = ev.getX();
float evY = ev.getY();
float x = evX - mTaskViewOffset.x;
float y = evY - mTaskViewOffset.y;
if (mDragRequested) {
if (!mIsDragging) {
mIsDragging = Math.hypot(evX - mDownPos.x, evY - mDownPos.y) > mDragSlop;
}
if (mIsDragging) {
int width = mRv.getMeasuredWidth();
int height = mRv.getMeasuredHeight();
DropTarget currentDropTarget = null;
// Give priority to the current drop target to retain the touch handling
if (mLastDropTarget != null) {
if (mLastDropTarget.acceptsDrop((int) evX, (int) evY, width, height,
mRv.mSystemInsets, true /* isCurrentTarget */)) {
currentDropTarget = mLastDropTarget;
}
}
// Otherwise, find the next target to handle this event
if (currentDropTarget == null) {
for (DropTarget target : mDropTargets) {
if (target.acceptsDrop((int) evX, (int) evY, width, height,
mRv.mSystemInsets, false /* isCurrentTarget */)) {
//查找当前的拖放点
currentDropTarget = target;
break;
}
}
}
if (mLastDropTarget != currentDropTarget) {
mLastDropTarget = currentDropTarget;
//通知拖放点变化
EventBus.getDefault().send(new DragDropTargetChangedEvent(mDragTask,
currentDropTarget));
}
}
//移动选中的任务
mTaskView.setTranslationX(x);
mTaskView.setTranslationY(y);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
if (mDragRequested) {
boolean cancelled = action == MotionEvent.ACTION_CANCEL;
if (cancelled) {
EventBus.getDefault().send(new DragDropTargetChangedEvent(mDragTask, null));
}
//触摸抬起, 发送事件后, 开始切换至分屏模式
EventBus.getDefault().send(new DragEndEvent(mDragTask, mTaskView,
!cancelled ? mLastDropTarget : null));
break;
}
}
}
}
- 切换至分屏模式
|-- frameworks/base/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
public final void onBusEvent(final DragEndEvent event) {
// Handle the case where we drop onto a dock region
if (event.dropTarget instanceof TaskStack.DockState) {
final TaskStack.DockState dockState = (TaskStack.DockState) event.dropTarget;
// Hide the dock region
updateVisibleDockRegions(null, false /* isDefaultDockState */, -1, -1,
false /* animateAlpha */, false /* animateBounds */);
// We translated the view but we need to animate it back from the current layout-space
// rect to its final layout-space rect
Utilities.setViewFrameFromTranslation(event.taskView);
// Dock the task and launch it 放置并以新的模式启动
//dockState.createMode的值, 分别对应屏后显示的位置.
//import static android.view.WindowManager.DOCKED_BOTTOM;
//import static android.view.WindowManager.DOCKED_INVALID;
//import static android.view.WindowManager.DOCKED_LEFT;
//import static android.view.WindowManager.DOCKED_RIGHT;
//import static android.view.WindowManager.DOCKED_TOP;
SystemServicesProxy ssp = Recents.getSystemServices();
if (ssp.startTaskInDockedMode(event.task.key.id, dockState.createMode)) {
final OnAnimationStartedListener startedListener =
new OnAnimationStartedListener() {
@Override
public void onAnimationStarted() {
EventBus.getDefault().send(new DockedFirstAnimationFrameEvent());
// Remove the task and don't bother relaying out, as all the tasks will be
// relaid out when the stack changes on the multiwindow change event
getStack().removeTask(event.task, null, true /* fromDockGesture */);
}
};
final Rect taskRect = getTaskRect(event.taskView);
IAppTransitionAnimationSpecsFuture future =
mTransitionHelper.getAppTransitionFuture(
new AnimationSpecComposer() {
@Override
public List<AppTransitionAnimationSpec> composeSpecs() {
return mTransitionHelper.composeDockAnimationSpec(
event.taskView, taskRect);
}
});
ssp.overridePendingAppTransitionMultiThumbFuture(future,
mTransitionHelper.wrapStartedListener(startedListener),
true /* scaleUp */);
MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_DRAG_DROP,
event.task.getTopComponent().flattenToShortString());
} else {
EventBus.getDefault().send(new DragEndCancelledEvent(getStack(), event.task,
event.taskView));
}
} else {
// Animate the overlay alpha back to 0
updateVisibleDockRegions(null, true /* isDefaultDockState */, -1, -1,
true /* animateAlpha */, false /* animateBounds */);
}
// Show the stack action button again without changing visibility
if (mStackActionButton != null) {
mStackActionButton.animate()
.alpha(1f)
.setDuration(SHOW_STACK_ACTION_BUTTON_DURATION)
.setInterpolator(Interpolators.ALPHA_IN)
.start();
}
}
- 调用startActivityFromRecents进入应用分屏模式
|-- frameworks/base/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
/** Docks a task to the side of the screen and starts it. */
public boolean startTaskInDockedMode(int taskId, int createMode) {
if (mIam == null) return false;
try {
final ActivityOptions options = ActivityOptions.makeBasic();
options.setDockCreateMode(createMode);
options.setLaunchStackId(DOCKED_STACK_ID);
mIam.startActivityFromRecents(taskId, options.toBundle());
return true;
} catch (Exception e) {
Log.e(TAG, "Failed to dock task: " + taskId + " with createMode: " + createMode, e);
}
return false;
}
与前面另一编文章分析Freeform模式的方式大同小异, 本文中增加了触摸部分代码的整理记录.
扩展
- 以指定分屏模式启动指定应用(仅测过DOCK 和 FREEFORM 模式):
//import static android.view.WindowManager.DOCKED_BOTTOM;
//import static android.view.WindowManager.DOCKED_INVALID;
//import static android.view.WindowManager.DOCKED_LEFT;
//import static android.view.WindowManager.DOCKED_RIGHT;
//import static android.view.WindowManager.DOCKED_TOP;
public static final int DOCKED_INVALID = -1;
public static final int DOCKED_LEFT = 1;
public static final int DOCKED_TOP = 2;
public static final int DOCKED_RIGHT = 3;
public static final int DOCKED_BOTTOM = 4;
//android.app.ActivityManager.StackId.DOCKED_STACK_ID
/** Invalid stack ID. */
public static final int INVALID_STACK_ID = -1;
/** First static stack ID. */
public static final int FIRST_STATIC_STACK_ID = 0;
/** Home activity stack ID. */
public static final int HOME_STACK_ID = FIRST_STATIC_STACK_ID;
/** ID of stack where fullscreen activities are normally launched into. */
public static final int FULLSCREEN_WORKSPACE_STACK_ID = 1;
/** ID of stack where freeform/resized activities are normally launched into. */
public static final int FREEFORM_WORKSPACE_STACK_ID = FULLSCREEN_WORKSPACE_STACK_ID + 1;
/** ID of stack that occupies a dedicated region of the screen. */
public static final int DOCKED_STACK_ID = FREEFORM_WORKSPACE_STACK_ID + 1;
/** ID of stack that always on top (always visible) when it exist. */
public static final int PINNED_STACK_ID = DOCKED_STACK_ID + 1;
@SuppressLint("NewApi")
public static boolean startActivityForMultiScreen(Context context, Intent intent, int stackId, int dockId, Rect bounds){
final ActivityOptions options;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
options = ActivityOptions.makeBasic();
Class AOP = ActivityOptions.class;
try {
//options.setDockCreateMode(createMode);
Method setDockCreateMode = AOP.getDeclaredMethod("setDockCreateMode", Integer.TYPE);
if(setDockCreateMode != null)setDockCreateMode.invoke(options, dockId);
//options.setLaunchStackId();
Method setLaunchStackId = AOP.getDeclaredMethod("setLaunchStackId", Integer.TYPE);
if(setLaunchStackId != null)setLaunchStackId.invoke(options, stackId);
if(bounds != null && !bounds.isEmpty()){
//need Android N
options.setLaunchBounds(bounds.isEmpty() ? null : bounds);
}
context.startActivity(intent, options.toBundle());
return true;
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
return false;
}