平台

RK3288 + Android 7.1

切入点

在点击近期任务后, 长按任务会出现如下界面, 拖动任务到指定区域可以进入分屏模式(DOCK):

48c25b4a39ff382296a00c246659ce8c.png

主窗体 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

"在此处拖动即可使用分屏功能"

字符使用

|-- 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();

}

}

5e217a44c920d999ab6ea729346b0393.png

后续拖动及触摸释放, 若释放前处理有效的分屏区域, 则启动进入分屏模式, 如上图

|-- 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 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;

}

Android 7.1 FreeForm 多窗口模式

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐