RK3288 + Android 7.1
Android N上的多窗口功能有三种模式:(扩展-4)
平台默认并没有打开这个模式的支持, 需要增加一个文件以打开Feeeform特性
增加 /system/etc/permissions/android.software.freeform_window_management.xml
<permissions>
<feature name="android.software.freeform_window_management" />
permissions>
打开后:
看任务右上角 X 旁边的图标
然而, 当尝试点击此按键后, 预想的画面并没有出现, 费解!
跟踪下源码中此界面的布局:
<com.android.systemui.recents.views.TaskViewHeader
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/task_view_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top|center_horizontal">
<com.android.systemui.recents.views.FixedSizeImageView
android:id="@+id/icon"
android:contentDescription="@string/recents_app_info_button_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingStart="16dp"
android:paddingEnd="12dp" />
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
android:textSize="16sp"
android:textColor="#ffffffff"
android:text="@string/recents_empty_message"
android:fontFamily="sans-serif-medium"
android:singleLine="true"
android:maxLines="1"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:forceHasOverlappingRendering="false" />
<com.android.systemui.recents.views.FixedSizeImageView
android:id="@+id/move_task"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:padding="@dimen/recents_task_view_header_button_padding"
android:src="@drawable/star"
android:background="?android:selectableItemBackground"
android:alpha="0"
android:visibility="gone" />
<com.android.systemui.recents.views.FixedSizeImageView
android:id="@+id/dismiss_task"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:padding="@dimen/recents_task_view_header_button_padding"
android:src="@drawable/recents_dismiss_light"
android:background="?android:selectableItemBackground"
android:alpha="0"
android:visibility="gone" />
<ViewStub android:id="@+id/focus_timer_indicator_stub"
android:inflatedId="@+id/focus_timer_indicator"
android:layout="@layout/recents_task_view_header_progress_bar"
android:layout_width="match_parent"
android:layout_height="5dp"
android:layout_gravity="bottom" />
<ViewStub android:id="@+id/app_overlay_stub"
android:inflatedId="@+id/app_overlay"
android:layout="@layout/recents_task_view_header_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent" />
com.android.systemui.recents.views.TaskViewHeader>
对应的自定义VIEW控件
/* The task bar view */
public class TaskViewHeader extends FrameLayout
implements View.OnClickListener, View.OnLongClickListener {
@Override
protected void onFinishInflate() {
SystemServicesProxy ssp = Recents.getSystemServices();
// Initialize the icon and description views
mIconView = (ImageView) findViewById(R.id.icon);
mIconView.setOnLongClickListener(this);
mTitleView = (TextView) findViewById(R.id.title);
mDismissButton = (ImageView) findViewById(R.id.dismiss_task);
if (ssp.hasFreeformWorkspaceSupport()) {
mMoveTaskButton = (ImageView) findViewById(R.id.move_task);
}
onConfigurationChanged();
}
@Override
public void onClick(View v) {
if (v == mIconView) {
//...
} else if (v == mMoveTaskButton) {
TaskView tv = Utilities.findParent(this, TaskView.class);
EventBus.getDefault().send(new LaunchTaskEvent(tv, mTask, null,
mMoveTaskTargetStackId, false));
} else if (v == mAppInfoView) {
//...
}
}
}
重点关注点击的实现的事件
关于LaunchTaskEvent
public class LaunchTaskEvent extends EventBus.Event {
public final TaskView taskView;
public final Task task;
public final Rect targetTaskBounds;
public final int targetTaskStack;
public final boolean screenPinningRequested;
public LaunchTaskEvent(TaskView taskView, Task task, Rect targetTaskBounds, int targetTaskStack,
boolean screenPinningRequested) {
this.taskView = taskView;
this.task = task;
this.targetTaskBounds = targetTaskBounds;
this.targetTaskStack = targetTaskStack;
this.screenPinningRequested = screenPinningRequested;
}
}
检测是否支持自由窗口模式
/** Private constructor */
private SystemServicesProxy(Context context) {
mAccm = AccessibilityManager.getInstance(context);
mAm = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
mIam = ActivityManagerNative.getDefault();
mPm = context.getPackageManager();
mIpm = AppGlobals.getPackageManager();
mAssistUtils = new AssistUtils(context);
mWm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mIwm = WindowManagerGlobal.getWindowManagerService();
mUm = UserManager.get(context);
mDisplay = mWm.getDefaultDisplay();
mRecentsPackage = context.getPackageName();
mHasFreeformWorkspaceSupport =
mPm.hasSystemFeature(PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT) ||
Settings.Global.getInt(context.getContentResolver(),
DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0;
}
/**
* Returns whether this device has freeform workspaces.
*/
public boolean hasFreeformWorkspaceSupport() {
return mHasFreeformWorkspaceSupport;
}
点击后, 加入事件队列
/**
* Sends an event to the subscribers of the given event type immediately. This can only be
* called from the same thread as the EventBus's looper thread (for the default EventBus, this
* is the main application thread).
*/
public void send(Event event) {
// Fail immediately if we are being called from the non-main thread
//...
queueEvent(event);
}
/**
* Processes and dispatches the given event to the given event handler, on the thread of whoever
* calls this method.
*/
private void processEvent(final EventHandler eventHandler, final Event event) {
//...反射调用.
eventHandler.method.invoke(sub, event);
//...
}
eventHandler的由来:
private static final String METHOD_PREFIX = "onBusEvent";
public void register(Object subscriber) {
registerSubscriber(subscriber, DEFAULT_SUBSCRIBER_PRIORITY, null);
}
public void register(Object subscriber, int priority) {
registerSubscriber(subscriber, priority, null);
}
/**
* Registers a new subscriber.
*/
private void registerSubscriber(Object subscriber, int priority,
MutableBoolean hasInterprocessEventsChangedOut) {
//...
// Find all the valid event bus handler methods of the subscriber
MutableBoolean isInterprocessEvent = new MutableBoolean(false);
Method[] methods = subscriberType.getDeclaredMethods();
for (Method m : methods) {
Class<?>[] parameterTypes = m.getParameterTypes();
isInterprocessEvent.value = false;
if (isValidEventBusHandlerMethod(m, parameterTypes, isInterprocessEvent)) {
Class<? extends Event> eventType = (Class<? extends Event>) parameterTypes[0];
ArrayList<EventHandler> eventTypeHandlers = mEventTypeMap.get(eventType);
if (eventTypeHandlers == null) {
eventTypeHandlers = new ArrayList<>();
mEventTypeMap.put(eventType, eventTypeHandlers);
}
if (isInterprocessEvent.value) {
try {
// Enforce that the event must have a Bundle constructor
eventType.getConstructor(Bundle.class);
mInterprocessEventNameMap.put(eventType.getName(),
(Class<? extends InterprocessEvent>) eventType);
if (hasInterprocessEventsChangedOut != null) {
hasInterprocessEventsChangedOut.value = true;
}
} catch (NoSuchMethodException e) {
throw new RuntimeException("Expected InterprocessEvent to have a Bundle constructor");
}
}
EventHandlerMethod method = new EventHandlerMethod(m, eventType);
EventHandler handler = new EventHandler(sub, method, priority);
eventTypeHandlers.add(handler);
//保存函数
subscriberMethods.add(method);
sortEventHandlersByPriority(eventTypeHandlers);
if (DEBUG_TRACE_ALL) {
logWithPid(" * Method: " + m.getName() +
" event: " + parameterTypes[0].getSimpleName() +
" interprocess? " + isInterprocessEvent.value);
}
}
}
//...
}
//检测对应的方法
/**
* @return whether {@param method} is a valid (normal or interprocess) event bus handler method
*/
private boolean isValidEventBusHandlerMethod(Method method, Class<?>[] parameterTypes,
MutableBoolean isInterprocessEventOut) {
int modifiers = method.getModifiers();
if (Modifier.isPublic(modifiers) &&
Modifier.isFinal(modifiers) &&
method.getReturnType().equals(Void.TYPE) &&
parameterTypes.length == 1) {
if (EventBus.InterprocessEvent.class.isAssignableFrom(parameterTypes[0]) &&
method.getName().startsWith(INTERPROCESS_METHOD_PREFIX)) {
isInterprocessEventOut.value = true;
return true;
} else if (EventBus.Event.class.isAssignableFrom(parameterTypes[0]) &&
method.getName().startsWith(METHOD_PREFIX)) {
isInterprocessEventOut.value = false;
return true;
} else {
if (DEBUG_TRACE_ALL) {
if (!EventBus.Event.class.isAssignableFrom(parameterTypes[0])) {
logWithPid(" Expected method take an Event-based parameter: " + method.getName());
} else if (!method.getName().startsWith(INTERPROCESS_METHOD_PREFIX) &&
!method.getName().startsWith(METHOD_PREFIX)) {
logWithPid(" Expected method start with method prefix: " + method.getName());
}
}
}
} else {
if (DEBUG_TRACE_ALL) {
if (!Modifier.isPublic(modifiers)) {
logWithPid(" Expected method to be public: " + method.getName());
} else if (!Modifier.isFinal(modifiers)) {
logWithPid(" Expected method to be final: " + method.getName());
} else if (!method.getReturnType().equals(Void.TYPE)) {
logWithPid(" Expected method to return null: " + method.getName());
}
}
}
return false;
}
处理事件, 开始执行切换
public final void onBusEvent(LaunchTaskEvent event) {
mLastTaskLaunchedWasFreeform = event.task.isFreeformTask();
mTransitionHelper.launchTaskFromRecents(getStack(), event.task, mTaskStackView,
event.taskView, event.screenPinningRequested, event.targetTaskBounds,
event.targetTaskStack);
}
/**
* Launches the specified {@link Task}.
*/
public void launchTaskFromRecents(final TaskStack stack, @Nullable final Task task,
final TaskStackView stackView, final TaskView taskView,
final boolean screenPinningRequested, final Rect bounds, final int destinationStack) {
final ActivityOptions opts = ActivityOptions.makeBasic();
if (bounds != null) {
opts.setLaunchBounds(bounds.isEmpty() ? null : bounds);
}
//...
if (taskView == null) {
// If there is no task view, then we do not need to worry about animating out occluding
// task views, and we can launch immediately
startTaskActivity(stack, task, taskView, opts, transitionFuture, animStartedListener);
} else {
LaunchTaskStartedEvent launchStartedEvent = new LaunchTaskStartedEvent(taskView,
screenPinningRequested);
if (task.group != null && !task.group.isFrontMostTask(task)) {
launchStartedEvent.addPostAnimationCallback(new Runnable() {
@Override
public void run() {
startTaskActivity(stack, task, taskView, opts, transitionFuture,
animStartedListener);
}
});
EventBus.getDefault().send(launchStartedEvent);
} else {
EventBus.getDefault().send(launchStartedEvent);
startTaskActivity(stack, task, taskView, opts, transitionFuture,
animStartedListener);
}
}
Recents.getSystemServices().sendCloseSystemWindows(
BaseStatusBar.SYSTEM_DIALOG_REASON_HOME_KEY);
}
/**
* Starts the activity for the launch task.
*
* @param taskView this is the {@link TaskView} that we are launching from. This can be null if
* we are toggling recents and the launch-to task is now offscreen.
*/
private void startTaskActivity(TaskStack stack, Task task, @Nullable TaskView taskView,
ActivityOptions opts, IAppTransitionAnimationSpecsFuture transitionFuture,
final ActivityOptions.OnAnimationStartedListener animStartedListener) {
SystemServicesProxy ssp = Recents.getSystemServices();
if (ssp.startActivityFromRecents(mContext, task.key, task.title, opts)) {
// Keep track of the index of the task launch
int taskIndexFromFront = 0;
int taskIndex = stack.indexOfStackTask(task);
if (taskIndex > -1) {
taskIndexFromFront = stack.getTaskCount() - taskIndex - 1;
}
EventBus.getDefault().send(new LaunchTaskSucceededEvent(taskIndexFromFront));
} else {
// Dismiss the task if we fail to launch it
if (taskView != null) {
taskView.dismissTask();
}
// Keep track of failed launches
EventBus.getDefault().send(new LaunchTaskFailedEvent());
}
if (transitionFuture != null) {
ssp.overridePendingAppTransitionMultiThumbFuture(transitionFuture,
wrapStartedListener(animStartedListener), true /* scaleUp */);
}
}
/** Starts an activity from recents. */
public boolean startActivityFromRecents(Context context, Task.TaskKey taskKey, String taskName,
ActivityOptions options) {
if (mIam != null) {
try {
if (taskKey.stackId == DOCKED_STACK_ID) {
// We show non-visible docked tasks in Recents, but we always want to launch
// them in the fullscreen stack.
if (options == null) {
options = ActivityOptions.makeBasic();
}
options.setLaunchStackId(FULLSCREEN_WORKSPACE_STACK_ID);
}
mIam.startActivityFromRecents(
taskKey.id, options == null ? null : options.toBundle());
return true;
} catch (Exception e) {
Log.e(TAG, context.getString(R.string.recents_launch_error_message, taskName), e);
}
}
return false;
}
进入ActivityManagerService并切换
public final int startActivityFromRecents(int taskId, Bundle bOptions)
final int startActivityFromRecentsInner(int taskId, Bundle bOptions)
//frameworks/base/packages/SystemUI/src/com/android/systemui/recents/views/RecentsTransitionHelper.java
public void launchTaskFromRecents(final TaskStack stack, @Nullable final Task task,
final TaskStackView stackView, final TaskView taskView,
final boolean screenPinningRequested, final Rect bounds, final int destinationStack) {
final ActivityOptions opts = ActivityOptions.makeBasic();
//----新增代码----
opts.setLaunchStackId(destinationStack);
if (bounds != null) {
opts.setLaunchBounds(bounds.isEmpty() ? null : bounds);
}
//...
}
编译并更新SystemUI, 完成!
//frameworks/base/core/java/android/app/ActivityManager.java
/** 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;