平台
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; }
Android 7.1 FreeForm 多窗口模式