Move PiP via DPAD to the four corners (TV)

Bug: 203641195
Test: manual
Change-Id: Ib2381ae6ff0b43f444af8e984a4f0e6d9786169b
This commit is contained in:
Jacqueline Bronger
2021-10-27 10:22:46 +00:00
parent 72cbf68ac8
commit f5863c14b1
21 changed files with 789 additions and 255 deletions

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="alpha"
android:valueTo="1"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:duration="100" />

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="alpha"
android:valueTo="0"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:duration="100" />

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="alpha"
android:valueTo="1"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:duration="100" />

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="alpha"
android:valueTo="0"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:duration="100" />

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2021 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/tv_pip_menu_focus_border"
android:pathData="M7,10l5,5 5,-5H7z"/>
</vector>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2021 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/tv_pip_menu_focus_border"
android:pathData="M14,7l-5,5 5,5V7z"/>
</vector>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2021 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/tv_pip_menu_focus_border"
android:pathData="M10,17l5,-5 -5,-5v10z"/>
</vector>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2021 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/tv_pip_menu_focus_border"
android:pathData="M7,14l5,-5 5,5H7z"/>
</vector>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2021 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11,5.83L11,10h2L13,5.83l1.83,1.83 1.41,-1.42L12,2 7.76,6.24l1.41,1.42zM17.76,7.76l-1.42,1.41L18.17,11L14,11v2h4.17l-1.83,1.83 1.42,1.41L22,12zM13,18.17L13,14h-2v4.17l-1.83,-1.83 -1.41,1.42L12,22l4.24,-4.24 -1.41,-1.42zM10,13v-2L5.83,11l1.83,-1.83 -1.42,-1.41L2,12l4.24,4.24 1.42,-1.41L5.83,13z"
android:fillColor="#FFFFFF" />
</vector>

View File

@@ -15,57 +15,101 @@
limitations under the License.
-->
<!-- Layout for TvPipMenuView -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tv_pip_menu"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
android:gravity="center"
android:scrollbars="none"
android:layout_centerInParent="true"
android:layout_margin="@dimen/pip_menu_outer_space">
<LinearLayout
android:id="@+id/tv_pip_menu_action_buttons"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingStart="@dimen/pip_menu_button_wrapper_margin"
android:paddingEnd="@dimen/pip_menu_button_wrapper_margin"
android:gravity="center"
android:orientation="horizontal"
android:alpha="0">
<com.android.wm.shell.pip.tv.TvPipMenuActionButton
android:id="@+id/tv_pip_menu_fullscreen_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/pip_ic_fullscreen_white"
android:text="@string/pip_fullscreen" />
<com.android.wm.shell.pip.tv.TvPipMenuActionButton
android:id="@+id/tv_pip_menu_move_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/pip_ic_move_white"
android:text="@String/pip_move" />
<com.android.wm.shell.pip.tv.TvPipMenuActionButton
android:id="@+id/tv_pip_menu_close_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/pip_ic_close_white"
android:text="@string/pip_close" />
<!-- More TvPipMenuActionButtons may be added here at runtime. -->
</LinearLayout>
</HorizontalScrollView>
<View
android:id="@+id/tv_pip_menu_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0" >
android:alpha="0"
android:layout_margin="@dimen/pip_menu_outer_space_frame"
android:background="@drawable/tv_pip_menu_border"/>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
android:gravity="center"
android:scrollbars="none"
android:requiresFadingEdge="vertical"
android:fadingEdgeLength="30dp">
<ImageView
android:id="@+id/tv_pip_menu_arrow_up"
android:layout_width="@dimen/pip_menu_arrow_size"
android:layout_height="@dimen/pip_menu_arrow_size"
android:layout_centerHorizontal="true"
android:layout_alignParentTop="true"
android:alpha="0"
android:elevation="@dimen/pip_menu_arrow_elevation"
android:src="@drawable/pip_ic_move_up" />
<LinearLayout
android:id="@+id/tv_pip_menu_action_buttons"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingStart="@dimen/pip_menu_button_wrapper_margin"
android:paddingEnd="@dimen/pip_menu_button_wrapper_margin"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/tv_pip_menu_arrow_right"
android:layout_width="@dimen/pip_menu_arrow_size"
android:layout_height="@dimen/pip_menu_arrow_size"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:alpha="0"
android:elevation="@dimen/pip_menu_arrow_elevation"
android:src="@drawable/pip_ic_move_right" />
<com.android.wm.shell.pip.tv.TvPipMenuActionButton
android:id="@+id/tv_pip_menu_fullscreen_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/pip_ic_fullscreen_white"
android:text="@string/pip_fullscreen" />
<ImageView
android:id="@+id/tv_pip_menu_arrow_down"
android:layout_width="@dimen/pip_menu_arrow_size"
android:layout_height="@dimen/pip_menu_arrow_size"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:alpha="0"
android:elevation="@dimen/pip_menu_arrow_elevation"
android:src="@drawable/pip_ic_move_down" />
<com.android.wm.shell.pip.tv.TvPipMenuActionButton
android:id="@+id/tv_pip_menu_close_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/pip_ic_close_white"
android:text="@string/pip_close" />
<!-- More TvPipMenuActionButtons may be added here at runtime. -->
</LinearLayout>
</HorizontalScrollView>
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/tv_pip_menu_border"/>
</FrameLayout>
</FrameLayout>
<ImageView
android:id="@+id/tv_pip_menu_arrow_left"
android:layout_width="@dimen/pip_menu_arrow_size"
android:layout_height="@dimen/pip_menu_arrow_size"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:alpha="0"
android:elevation="@dimen/pip_menu_arrow_elevation"
android:src="@drawable/pip_ic_move_left" />
</RelativeLayout>

View File

@@ -21,7 +21,14 @@
<dimen name="pip_menu_icon_size">20dp</dimen>
<dimen name="pip_menu_button_margin">4dp</dimen>
<dimen name="pip_menu_button_wrapper_margin">26dp</dimen>
<dimen name="pip_menu_border_width">2dp</dimen>
<dimen name="pip_menu_border_radius">0dp</dimen>
<dimen name="pip_menu_border_width">4dp</dimen>
<dimen name="pip_menu_border_radius">4dp</dimen>
<dimen name="pip_menu_outer_space">24dp</dimen>
<!-- outer space minus border width -->
<dimen name="pip_menu_outer_space_frame">20dp</dimen>
<dimen name="pip_menu_arrow_size">24dp</dimen>
<dimen name="pip_menu_arrow_elevation">5dp</dimen>
</resources>

View File

@@ -19,6 +19,6 @@
<color name="tv_pip_menu_icon_unfocused">#E8EAED</color>
<color name="tv_pip_menu_icon_disabled">#80868B</color>
<color name="tv_pip_menu_icon_bg_focused">#E8EAED</color>
<color name="tv_pip_menu_icon_bg_unfocused">#777777</color>
<color name="tv_pip_menu_focus_border">#CCE8EAED</color>
<color name="tv_pip_menu_icon_bg_unfocused">#990E0E0F</color>
<color name="tv_pip_menu_focus_border">#E8EAED</color>
</resources>

View File

@@ -30,5 +30,8 @@
<!-- Button to move picture-in-picture (PIP) screen to the fullscreen in PIP menu [CHAR LIMIT=30] -->
<string name="pip_fullscreen">Full screen</string>
<!-- Button to move picture-in-picture (PIP) via DPAD in the PIP menu [CHAR LIMIT=30] -->
<string name="pip_move">Move PIP</string>
</resources>

View File

@@ -30,7 +30,6 @@ import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.pip.PipAnimationController;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
import com.android.wm.shell.pip.PipMediaController;
import com.android.wm.shell.pip.PipSnapAlgorithm;
@@ -39,6 +38,7 @@ import com.android.wm.shell.pip.PipTaskOrganizer;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip.PipTransitionState;
import com.android.wm.shell.pip.PipUiEventLogger;
import com.android.wm.shell.pip.tv.TvPipBoundsAlgorithm;
import com.android.wm.shell.pip.tv.TvPipController;
import com.android.wm.shell.pip.tv.TvPipMenuController;
import com.android.wm.shell.pip.tv.TvPipNotificationController;
@@ -61,7 +61,7 @@ public abstract class TvPipModule {
static Optional<Pip> providePip(
Context context,
PipBoundsState pipBoundsState,
PipBoundsAlgorithm pipBoundsAlgorithm,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
PipTaskOrganizer pipTaskOrganizer,
TvPipMenuController tvPipMenuController,
PipMediaController pipMediaController,
@@ -74,7 +74,7 @@ public abstract class TvPipModule {
TvPipController.create(
context,
pipBoundsState,
pipBoundsAlgorithm,
tvPipBoundsAlgorithm,
pipTaskOrganizer,
pipTransitionController,
tvPipMenuController,
@@ -93,9 +93,9 @@ public abstract class TvPipModule {
@WMSingleton
@Provides
static PipBoundsAlgorithm providePipBoundsAlgorithm(Context context,
static TvPipBoundsAlgorithm provideTvPipBoundsAlgorithm(Context context,
PipBoundsState pipBoundsState, PipSnapAlgorithm pipSnapAlgorithm) {
return new PipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm);
return new TvPipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm);
}
@WMSingleton
@@ -109,10 +109,11 @@ public abstract class TvPipModule {
@Provides
static PipTransitionController provideTvPipTransition(
Transitions transitions, ShellTaskOrganizer shellTaskOrganizer,
PipAnimationController pipAnimationController, PipBoundsAlgorithm pipBoundsAlgorithm,
PipAnimationController pipAnimationController,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
PipBoundsState pipBoundsState, TvPipMenuController pipMenuController) {
return new TvPipTransition(pipBoundsState, pipMenuController,
pipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer);
tvPipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer);
}
@WMSingleton
@@ -156,7 +157,7 @@ public abstract class TvPipModule {
SyncTransactionQueue syncTransactionQueue,
PipBoundsState pipBoundsState,
PipTransitionState pipTransitionState,
PipBoundsAlgorithm pipBoundsAlgorithm,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
PipAnimationController pipAnimationController,
PipTransitionController pipTransitionController,
PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
@@ -166,7 +167,7 @@ public abstract class TvPipModule {
PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer,
@ShellMainThread ShellExecutor mainExecutor) {
return new PipTaskOrganizer(context,
syncTransactionQueue, pipTransitionState, pipBoundsState, pipBoundsAlgorithm,
syncTransactionQueue, pipTransitionState, pipBoundsState, tvPipBoundsAlgorithm,
tvPipMenuController, pipAnimationController, pipSurfaceTransactionHelper,
pipTransitionController, splitScreenOptional, newSplitScreenOptional,
displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor);

View File

@@ -44,7 +44,7 @@ public class PipBoundsAlgorithm {
private static final String TAG = PipBoundsAlgorithm.class.getSimpleName();
private static final float INVALID_SNAP_FRACTION = -1f;
private final @NonNull PipBoundsState mPipBoundsState;
protected final @NonNull PipBoundsState mPipBoundsState;
private final PipSnapAlgorithm mSnapAlgorithm;
private float mDefaultSizePercent;

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.wm.shell.pip.tv;
import android.content.Context;
import android.graphics.Rect;
import android.util.Log;
import android.view.Gravity;
import androidx.annotation.NonNull;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
import com.android.wm.shell.pip.PipSnapAlgorithm;
/**
* Contains pip bounds calculations that are specific to TV.
*/
public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm {
private static final String TAG = TvPipBoundsAlgorithm.class.getSimpleName();
private static final boolean DEBUG = false;
public TvPipBoundsAlgorithm(Context context,
@NonNull PipBoundsState pipBoundsState,
@NonNull PipSnapAlgorithm pipSnapAlgorithm) {
super(context, pipBoundsState, pipSnapAlgorithm);
}
/**
* The normal bounds at a different position on the screen.
*/
public Rect getTvNormalBounds(int gravity) {
Rect normalBounds = getNormalBounds();
Rect insetBounds = new Rect();
getInsetBounds(insetBounds);
if (mPipBoundsState.isImeShowing()) {
if (DEBUG) Log.d(TAG, "IME showing, height: " + mPipBoundsState.getImeHeight());
insetBounds.bottom -= mPipBoundsState.getImeHeight();
}
Rect result = new Rect();
Gravity.apply(gravity, normalBounds.width(), normalBounds.height(), insetBounds, result);
if (DEBUG) {
Log.d(TAG, "normalBounds: " + normalBounds.toShortString());
Log.d(TAG, "insetBounds: " + insetBounds.toShortString());
Log.d(TAG, "gravity: " + Gravity.toString(gravity));
Log.d(TAG, "resultBounds: " + result.toShortString());
}
return result;
}
}

View File

@@ -18,6 +18,10 @@ package com.android.wm.shell.pip.tv;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.view.KeyEvent.KEYCODE_DPAD_DOWN;
import static android.view.KeyEvent.KEYCODE_DPAD_LEFT;
import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
import static android.view.KeyEvent.KEYCODE_DPAD_UP;
import android.annotation.IntDef;
import android.app.ActivityManager;
@@ -33,6 +37,7 @@ import android.graphics.Rect;
import android.os.RemoteException;
import android.util.Log;
import android.view.DisplayInfo;
import android.view.Gravity;
import com.android.wm.shell.R;
import com.android.wm.shell.WindowManagerShellWrapper;
@@ -42,7 +47,6 @@ import com.android.wm.shell.common.TaskStackListenerCallback;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.pip.PinnedStackListenerForwarder;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
import com.android.wm.shell.pip.PipMediaController;
import com.android.wm.shell.pip.PipTaskOrganizer;
@@ -85,10 +89,12 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
*/
private static final int STATE_PIP_MENU = 2;
private static final int DEFAULT_GRAVITY = Gravity.BOTTOM | Gravity.RIGHT;
private final Context mContext;
private final PipBoundsState mPipBoundsState;
private final PipBoundsAlgorithm mPipBoundsAlgorithm;
private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
private final PipTaskOrganizer mPipTaskOrganizer;
private final PipMediaController mPipMediaController;
private final TvPipNotificationController mPipNotificationController;
@@ -97,6 +103,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
private final TvPipImpl mImpl = new TvPipImpl();
private @State int mState = STATE_NO_PIP;
private @Gravity.GravityFlags int mGravity = DEFAULT_GRAVITY;
private int mPinnedTaskId = NONEXISTENT_TASK_ID;
private int mResizeAnimationDuration;
@@ -104,7 +111,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
public static Pip create(
Context context,
PipBoundsState pipBoundsState,
PipBoundsAlgorithm pipBoundsAlgorithm,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
PipTaskOrganizer pipTaskOrganizer,
PipTransitionController pipTransitionController,
TvPipMenuController tvPipMenuController,
@@ -116,7 +123,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
return new TvPipController(
context,
pipBoundsState,
pipBoundsAlgorithm,
tvPipBoundsAlgorithm,
pipTaskOrganizer,
pipTransitionController,
tvPipMenuController,
@@ -130,7 +137,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
private TvPipController(
Context context,
PipBoundsState pipBoundsState,
PipBoundsAlgorithm pipBoundsAlgorithm,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
PipTaskOrganizer pipTaskOrganizer,
PipTransitionController pipTransitionController,
TvPipMenuController tvPipMenuController,
@@ -145,7 +152,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
mPipBoundsState = pipBoundsState;
mPipBoundsState.setDisplayId(context.getDisplayId());
mPipBoundsState.setDisplayLayout(new DisplayLayout(context, context.getDisplay()));
mPipBoundsAlgorithm = pipBoundsAlgorithm;
mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm;
mPipMediaController = pipMediaController;
@@ -192,24 +199,19 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
public void showPictureInPictureMenu() {
if (DEBUG) Log.d(TAG, "showPictureInPictureMenu(), state=" + stateToName(mState));
if (mState != STATE_PIP) {
if (mState == STATE_NO_PIP) {
if (DEBUG) Log.d(TAG, " > cannot open Menu from the current state.");
return;
}
setState(STATE_PIP_MENU);
resizePinnedStack(STATE_PIP_MENU);
movePinnedStack();
}
/**
* Moves Pip window to its "normal" position.
*/
@Override
public void movePipToNormalPosition() {
if (DEBUG) Log.d(TAG, "movePipToNormalPosition(), state=" + stateToName(mState));
public void closeMenu() {
if (DEBUG) Log.d(TAG, "closeMenu(), state before=" + stateToName(mState));
setState(STATE_PIP);
resizePinnedStack(STATE_PIP);
}
/**
@@ -223,6 +225,69 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
onPipDisappeared();
}
@Override
public void movePip(int keycode) {
if (updatePosition(keycode)) {
if (DEBUG) Log.d(TAG, "New gravity: " + Gravity.toString(mGravity));
mTvPipMenuController.updateMenu(mGravity);
movePinnedStack();
} else {
if (DEBUG) Log.d(TAG, "Position hasn't changed");
}
}
@Override
public int getPipGravity() {
return mGravity;
}
/**
* @return true if position changed
*/
private boolean updatePosition(int keycode) {
if (DEBUG) Log.d(TAG, "updatePosition, keycode: " + keycode);
int updatedGravity;
switch (keycode) {
case KEYCODE_DPAD_UP:
updatedGravity = (mGravity & (~Gravity.BOTTOM)) | Gravity.TOP;
break;
case KEYCODE_DPAD_DOWN:
updatedGravity = (mGravity & (~Gravity.TOP)) | Gravity.BOTTOM;
break;
case KEYCODE_DPAD_LEFT:
updatedGravity = (mGravity & (~Gravity.RIGHT)) | Gravity.LEFT;
break;
case KEYCODE_DPAD_RIGHT:
updatedGravity = (mGravity & (~Gravity.LEFT)) | Gravity.RIGHT;
break;
default:
updatedGravity = mGravity;
}
if (updatedGravity != mGravity) {
mGravity = updatedGravity;
return true;
}
return false;
}
/**
* Animate to the updated position of the PiP based on the state and position of the PiP.
*/
private void movePinnedStack() {
if (mState == STATE_NO_PIP) {
return;
}
Rect bounds = mTvPipBoundsAlgorithm.getTvNormalBounds(mGravity);
if (DEBUG) Log.d(TAG, "movePinnedStack() - new pip bounds: " + bounds.toShortString());
mPipTaskOrganizer.scheduleAnimateResizePip(bounds,
mResizeAnimationDuration, rect -> {
if (DEBUG) Log.d(TAG, "movePinnedStack() animation done");
});
}
/**
* Closes Pip window.
*/
@@ -234,41 +299,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
onPipDisappeared();
}
/**
* Resizes the Pip task/window to the appropriate size for the given state.
* This is a legacy API. Now we expect that the state argument passed to it should always match
* the current state of the Controller. If it does not match an {@link IllegalArgumentException}
* will be thrown. However, if the passed state does match - we'll determine the right bounds
* to the state and will move Pip task/window there.
*
* @param state the to determine the Pip bounds. IMPORTANT: should always match the current
* state of the Controller.
*/
private void resizePinnedStack(@State int state) {
if (state != mState) {
throw new IllegalArgumentException("The passed state should match the current state!");
}
if (DEBUG) Log.d(TAG, "resizePinnedStack() state=" + stateToName(mState));
final Rect newBounds;
switch (mState) {
case STATE_PIP_MENU:
case STATE_PIP:
// Let PipBoundsAlgorithm figure out what the correct bounds are at the moment.
// Internally, it will get the "default" bounds from PipBoundsState and adjust them
// as needed to account for things like IME state (will query PipBoundsState for
// this information as well, so it's important to keep PipBoundsState up to date).
newBounds = mPipBoundsAlgorithm.getNormalBounds();
break;
case STATE_NO_PIP:
default:
return;
}
mPipTaskOrganizer.scheduleAnimateResizePip(newBounds, mResizeAnimationDuration, null);
}
private void registerSessionListenerForCurrentUser() {
mPipMediaController.registerSessionListenerForCurrentUser();
}
@@ -298,6 +328,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
mPipNotificationController.dismiss();
mTvPipMenuController.hideMenu();
mGravity = DEFAULT_GRAVITY;
setState(STATE_NO_PIP);
mPinnedTaskId = NONEXISTENT_TASK_ID;
}
@@ -384,10 +415,9 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
return;
}
mPipBoundsState.setImeVisibility(imeVisible, imeHeight);
// "Normal" Pip bounds may have changed, so if we are in the "normal" state,
// let's update the bounds.
if (mState == STATE_PIP) {
resizePinnedStack(STATE_PIP);
if (mState != STATE_NO_PIP) {
movePinnedStack();
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.wm.shell.pip.tv;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
/**
* All interpolators needed for TV specific Pip animations
*/
public class TvPipInterpolators {
/**
* A standard ease-in-out curve reserved for moments of interaction (button and card states).
*/
public static final Interpolator STANDARD = new PathInterpolator(0.2f, 0.1f, 0f, 1f);
/**
* A sharp ease-out-expo curve created for snappy but fluid browsing between cards and clusters.
*/
public static final Interpolator BROWSE = new PathInterpolator(0.18f, 1f, 0.22f, 1f);
/**
* A smooth ease-out-expo curve created for incoming elements (forward, back, overlay).
*/
public static final Interpolator ENTER = new PathInterpolator(0.12f, 1f, 0.4f, 1f);
/**
* A smooth ease-in-out-expo curve created for outgoing elements (forward, back, overlay).
*/
public static final Interpolator EXIT = new PathInterpolator(0.4f, 1f, 0.12f, 1f);
}

View File

@@ -24,13 +24,18 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ParceledListSlice;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Handler;
import android.os.RemoteException;
import android.util.Log;
import android.view.SurfaceControl;
import android.view.SyncRtSurfaceTransactionApplier;
import androidx.annotation.Nullable;
import com.android.wm.shell.R;
import com.android.wm.shell.common.SystemWindows;
import com.android.wm.shell.pip.PipBoundsState;
import com.android.wm.shell.pip.PipMediaController;
@@ -53,11 +58,33 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
private Delegate mDelegate;
private SurfaceControl mLeash;
private TvPipMenuView mMenuView;
private TvPipMenuView mPipMenuView;
// User can actively move the PiP via the DPAD.
private boolean mInMoveMode;
private final List<RemoteAction> mMediaActions = new ArrayList<>();
private final List<RemoteAction> mAppActions = new ArrayList<>();
private SyncRtSurfaceTransactionApplier mApplier;
RectF mTmpSourceRectF = new RectF();
RectF mTmpDestinationRectF = new RectF();
Matrix mMoveTransform = new Matrix();
private final float[] mTmpValues = new float[9];
private final Runnable mUpdateEmbeddedMatrix = () -> {
if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) {
return;
}
mMoveTransform.getValues(mTmpValues);
try {
mPipMenuView.getViewRootImpl().getAccessibilityEmbeddedConnection()
.setScreenMatrix(mTmpValues);
} catch (RemoteException e) {
if (DEBUG) e.printStackTrace();
}
};
public TvPipMenuController(Context context, PipBoundsState pipBoundsState,
SystemWindows systemWindows, PipMediaController pipMediaController,
Handler mainHandler) {
@@ -107,13 +134,13 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
private void attachPipMenuView() {
if (DEBUG) Log.d(TAG, "attachPipMenuView()");
if (mMenuView != null) {
if (mPipMenuView != null) {
detachPipMenuView();
}
mMenuView = new TvPipMenuView(mContext);
mMenuView.setListener(this);
mSystemWindows.addView(mMenuView,
mPipMenuView = new TvPipMenuView(mContext);
mPipMenuView.setListener(this);
mSystemWindows.addView(mPipMenuView,
getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */),
0, SHELL_ROOT_LAYER_PIP);
}
@@ -122,38 +149,76 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
public void showMenu() {
if (DEBUG) Log.d(TAG, "showMenu()");
if (mMenuView != null) {
Rect pipBounds = mPipBoundsState.getBounds();
mSystemWindows.updateViewLayout(mMenuView, getPipMenuLayoutParams(
MENU_WINDOW_TITLE, pipBounds.width(), pipBounds.height()));
if (mPipMenuView != null) {
Rect menuBounds = getMenuBounds(mPipBoundsState.getBounds());
mSystemWindows.updateViewLayout(mPipMenuView, getPipMenuLayoutParams(
MENU_WINDOW_TITLE, menuBounds.width(), menuBounds.height()));
maybeUpdateMenuViewActions();
SurfaceControl menuSurfaceControl = mSystemWindows.getViewSurface(mMenuView);
SurfaceControl menuSurfaceControl = mSystemWindows.getViewSurface(mPipMenuView);
if (menuSurfaceControl != null) {
SurfaceControl.Transaction t = new SurfaceControl.Transaction();
t.setRelativeLayer(mMenuView.getWindowSurfaceControl(), mLeash, 1);
t.setPosition(menuSurfaceControl, pipBounds.left, pipBounds.top);
t.setRelativeLayer(mPipMenuView.getWindowSurfaceControl(), mLeash, 1);
t.setPosition(menuSurfaceControl, menuBounds.left, menuBounds.top);
t.apply();
}
mMenuView.show();
mPipMenuView.show(mInMoveMode, mDelegate.getPipGravity());
}
}
void updateMenu(int gravity) {
mPipMenuView.showMovementHints(gravity);
}
private Rect getMenuBounds(Rect pipBounds) {
int extraSpaceInPx = mContext.getResources()
.getDimensionPixelSize(R.dimen.pip_menu_outer_space);
Rect menuBounds = new Rect(pipBounds);
menuBounds.inset(-extraSpaceInPx, -extraSpaceInPx);
return menuBounds;
}
void hideMenu() {
hideMenu(true);
if (!isMenuVisible()) {
if (DEBUG) Log.d(TAG, "hideMenu() - Menu isn't visible, so don't hide");
return;
} else {
if (DEBUG) Log.d(TAG, "hideMenu()");
}
mPipMenuView.hide(mInMoveMode);
if (!mInMoveMode) {
mDelegate.closeMenu();
}
}
void hideMenu(boolean movePipWindow) {
if (DEBUG) Log.d(TAG, "hideMenu(), movePipWindow=" + movePipWindow);
@Override
public void onEnterMoveMode() {
if (DEBUG) Log.d(TAG, "onEnterMoveMode - " + mInMoveMode);
mInMoveMode = true;
mPipMenuView.showMenuButtons(false);
mPipMenuView.showMovementHints(mDelegate.getPipGravity());
}
if (!isMenuVisible()) {
return;
@Override
public boolean onExitMoveMode() {
if (DEBUG) Log.d(TAG, "onExitMoveMode - " + mInMoveMode);
if (mInMoveMode) {
mInMoveMode = false;
mPipMenuView.showMenuButtons(true);
mPipMenuView.hideMovementHints();
return true;
}
return false;
}
mMenuView.hide();
if (movePipWindow) {
mDelegate.movePipToNormalPosition();
@Override
public boolean onPipMovement(int keycode) {
if (DEBUG) Log.d(TAG, "onPipMovement - " + mInMoveMode);
if (mInMoveMode) {
mDelegate.movePip(keycode);
}
return mInMoveMode;
}
@Override
@@ -163,17 +228,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
mLeash = null;
}
private void detachPipMenuView() {
if (DEBUG) Log.d(TAG, "detachPipMenuView()");
if (mMenuView == null) {
return;
}
mSystemWindows.removeView(mMenuView);
mMenuView = null;
}
@Override
public void setAppActions(ParceledListSlice<RemoteAction> actions) {
if (DEBUG) Log.d(TAG, "setAppActions()");
@@ -209,24 +263,146 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
}
private void maybeUpdateMenuViewActions() {
if (mMenuView == null) {
if (mPipMenuView == null) {
return;
}
if (!mAppActions.isEmpty()) {
mMenuView.setAdditionalActions(mAppActions, mMainHandler);
mPipMenuView.setAdditionalActions(mAppActions, mMainHandler);
} else {
mMenuView.setAdditionalActions(mMediaActions, mMainHandler);
mPipMenuView.setAdditionalActions(mMediaActions, mMainHandler);
}
}
@Override
public boolean isMenuVisible() {
return mMenuView != null && mMenuView.isVisible();
boolean isVisible = mPipMenuView != null && mPipMenuView.isVisible();
if (DEBUG) Log.d(TAG, "isMenuVisible: " + isVisible);
return isVisible;
}
/**
* Does an immediate window crop of the PiP menu.
*/
@Override
public void resizePipMenu(@android.annotation.Nullable SurfaceControl pipLeash,
@android.annotation.Nullable SurfaceControl.Transaction t,
Rect destinationBounds) {
if (DEBUG) Log.d(TAG, "resizePipMenu: " + destinationBounds.toShortString());
if (destinationBounds.isEmpty()) {
return;
}
if (!maybeCreateSyncApplier()) {
return;
}
SurfaceControl surfaceControl = getSurfaceControl();
SyncRtSurfaceTransactionApplier.SurfaceParams
params = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(surfaceControl)
.withWindowCrop(getMenuBounds(destinationBounds))
.build();
if (pipLeash != null && t != null) {
SyncRtSurfaceTransactionApplier.SurfaceParams
pipParams = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(pipLeash)
.withMergeTransaction(t)
.build();
mApplier.scheduleApply(params, pipParams);
} else {
mApplier.scheduleApply(params);
}
}
private SurfaceControl getSurfaceControl() {
return mSystemWindows.getViewSurface(mPipMenuView);
}
@Override
public void movePipMenu(SurfaceControl pipLeash, SurfaceControl.Transaction transaction,
Rect pipDestBounds) {
if (DEBUG) Log.d(TAG, "movePipMenu: " + pipDestBounds.toShortString());
if (pipDestBounds.isEmpty()) {
if (transaction == null && DEBUG) Log.d(TAG, "no transaction given");
return;
}
if (!maybeCreateSyncApplier()) {
return;
}
Rect menuDestBounds = getMenuBounds(pipDestBounds);
Rect mTmpSourceBounds = new Rect();
// If there is no pip leash supplied, that means the PiP leash is already finalized
// resizing and the PiP menu is also resized. We then want to do a scale from the current
// new menu bounds.
if (pipLeash != null && transaction != null) {
if (DEBUG) Log.d(TAG, "mTmpSourceBounds based on mPipMenuView.getBoundsOnScreen()");
mPipMenuView.getBoundsOnScreen(mTmpSourceBounds);
} else {
if (DEBUG) Log.d(TAG, "mTmpSourceBounds based on menu width and height");
mTmpSourceBounds.set(0, 0, menuDestBounds.width(), menuDestBounds.height());
}
mTmpSourceRectF.set(mTmpSourceBounds);
mTmpDestinationRectF.set(menuDestBounds);
mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
SurfaceControl surfaceControl = getSurfaceControl();
SyncRtSurfaceTransactionApplier.SurfaceParams params =
new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(
surfaceControl).withMatrix(mMoveTransform).build();
if (pipLeash != null && transaction != null) {
SyncRtSurfaceTransactionApplier.SurfaceParams
pipParams = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(pipLeash)
.withMergeTransaction(transaction)
.build();
mApplier.scheduleApply(params, pipParams);
} else {
mApplier.scheduleApply(params);
}
if (mPipMenuView.getViewRootImpl() != null) {
mPipMenuView.getHandler().removeCallbacks(mUpdateEmbeddedMatrix);
mPipMenuView.getHandler().post(mUpdateEmbeddedMatrix);
}
}
private boolean maybeCreateSyncApplier() {
if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) {
Log.v(TAG, "Not going to move PiP, either menu or its parent is not created.");
return false;
}
if (mApplier == null) {
mApplier = new SyncRtSurfaceTransactionApplier(mPipMenuView);
}
return true;
}
private void detachPipMenuView() {
if (mPipMenuView == null) {
return;
}
mApplier = null;
mSystemWindows.removeView(mPipMenuView);
mPipMenuView = null;
}
@Override
public void updateMenuBounds(Rect destinationBounds) {
Rect menuBounds = getMenuBounds(destinationBounds);
if (DEBUG) Log.d(TAG, "updateMenuBounds: " + menuBounds.toShortString());
mSystemWindows.updateViewLayout(mPipMenuView,
getPipMenuLayoutParams(MENU_WINDOW_TITLE, menuBounds.width(),
menuBounds.height()));
}
@Override
public void onBackPress() {
hideMenu();
if (!onExitMoveMode()) {
hideMenu();
}
}
@Override
@@ -240,8 +416,14 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
}
interface Delegate {
void movePipToNormalPosition();
void movePipToFullscreen();
void movePip(int keycode);
int getPipGravity();
void closeMenu();
void closePip();
}
}

View File

@@ -16,17 +16,21 @@
package com.android.wm.shell.pip.tv;
import static android.animation.AnimatorInflater.loadAnimator;
import static android.view.KeyEvent.ACTION_UP;
import static android.view.KeyEvent.KEYCODE_BACK;
import static android.view.KeyEvent.KEYCODE_DPAD_CENTER;
import static android.view.KeyEvent.KEYCODE_DPAD_DOWN;
import static android.view.KeyEvent.KEYCODE_DPAD_LEFT;
import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
import static android.view.KeyEvent.KEYCODE_DPAD_UP;
import android.animation.Animator;
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.content.Context;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.SurfaceControl;
@@ -34,6 +38,7 @@ import android.view.View;
import android.view.ViewRootImpl;
import android.view.WindowManagerGlobal;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
@@ -45,16 +50,14 @@ import java.util.ArrayList;
import java.util.List;
/**
* A View that represents Pip Menu on TV. It's responsible for displaying 2 ever-present Pip Menu
* actions: Fullscreen and Close, but could also display "additional" actions, that may be set via
* a {@link #setAdditionalActions(List, Handler)} call.
* A View that represents Pip Menu on TV. It's responsible for displaying 3 ever-present Pip Menu
* actions: Fullscreen, Move and Close, but could also display "additional" actions, that may be set
* via a {@link #setAdditionalActions(List, Handler)} call.
*/
public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
private static final String TAG = "TvPipMenuView";
private static final boolean DEBUG = TvPipController.DEBUG;
private final Animator mFadeInAnimation;
private final Animator mFadeOutAnimation;
@Nullable
private Listener mListener;
@@ -62,6 +65,11 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
private final View mMenuFrameView;
private final List<TvPipMenuActionButton> mAdditionalButtons = new ArrayList<>();
private final ImageView mArrowUp;
private final ImageView mArrowRight;
private final ImageView mArrowDown;
private final ImageView mArrowLeft;
public TvPipMenuView(@NonNull Context context) {
this(context, null);
}
@@ -85,35 +93,68 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
.setOnClickListener(this);
mActionButtonsContainer.findViewById(R.id.tv_pip_menu_close_button)
.setOnClickListener(this);
mActionButtonsContainer.findViewById(R.id.tv_pip_menu_move_button)
.setOnClickListener(this);
mMenuFrameView = findViewById(R.id.tv_pip_menu_frame);
mFadeInAnimation = loadAnimator(mContext, R.anim.tv_pip_menu_fade_in_animation);
mFadeInAnimation.setTarget(mMenuFrameView);
mFadeOutAnimation = loadAnimator(mContext, R.anim.tv_pip_menu_fade_out_animation);
mFadeOutAnimation.setTarget(mMenuFrameView);
mArrowUp = findViewById(R.id.tv_pip_menu_arrow_up);
mArrowRight = findViewById(R.id.tv_pip_menu_arrow_right);
mArrowDown = findViewById(R.id.tv_pip_menu_arrow_down);
mArrowLeft = findViewById(R.id.tv_pip_menu_arrow_left);
}
void setListener(@Nullable Listener listener) {
mListener = listener;
}
void show() {
if (DEBUG) Log.d(TAG, "show()");
mFadeInAnimation.start();
void show(boolean inMoveMode, int gravity) {
if (DEBUG) Log.d(TAG, "show(), inMoveMode: " + inMoveMode);
grantWindowFocus(true);
if (inMoveMode) {
showMovementHints(gravity);
} else {
animateAlphaTo(1, mActionButtonsContainer);
}
animateAlphaTo(1, mMenuFrameView);
}
void hide() {
void hide(boolean isInMoveMode) {
if (DEBUG) Log.d(TAG, "hide()");
animateAlphaTo(0, mActionButtonsContainer);
animateAlphaTo(0, mMenuFrameView);
hideMovementHints();
mFadeOutAnimation.start();
grantWindowFocus(false);
if (!isInMoveMode) {
grantWindowFocus(false);
}
}
private void animateAlphaTo(float alpha, View view) {
view.animate()
.alpha(alpha)
.setInterpolator(alpha == 0f ? TvPipInterpolators.EXIT : TvPipInterpolators.ENTER)
.setDuration(500)
.withStartAction(() -> {
if (alpha != 0) {
view.setVisibility(VISIBLE);
}
})
.withEndAction(() -> {
if (alpha == 0) {
view.setVisibility(GONE);
}
});
}
boolean isVisible() {
return mMenuFrameView != null && mMenuFrameView.getAlpha() != 0.0f;
return mMenuFrameView.getAlpha() != 0f
|| mActionButtonsContainer.getAlpha() != 0f
|| mArrowUp.getAlpha() != 0f
|| mArrowRight.getAlpha() != 0f
|| mArrowDown.getAlpha() != 0f
|| mArrowLeft.getAlpha() != 0f;
}
private void grantWindowFocus(boolean grantFocus) {
@@ -188,6 +229,8 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
final int id = v.getId();
if (id == R.id.tv_pip_menu_fullscreen_button) {
mListener.onFullscreenButtonClick();
} else if (id == R.id.tv_pip_menu_move_button) {
mListener.onEnterMoveMode();
} else if (id == R.id.tv_pip_menu_close_button) {
mListener.onCloseButtonClick();
} else {
@@ -207,17 +250,79 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener {
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getAction() == ACTION_UP && event.getKeyCode() == KEYCODE_BACK
&& mListener != null) {
mListener.onBackPress();
return true;
if (DEBUG) {
Log.d(TAG, "dispatchKeyEvent, action: " + event.getAction()
+ ", keycode: " + event.getKeyCode());
}
if (mListener != null && event.getAction() == ACTION_UP) {
switch (event.getKeyCode()) {
case KEYCODE_BACK:
mListener.onBackPress();
return true;
case KEYCODE_DPAD_UP:
case KEYCODE_DPAD_DOWN:
case KEYCODE_DPAD_LEFT:
case KEYCODE_DPAD_RIGHT:
return mListener.onPipMovement(event.getKeyCode()) || super.dispatchKeyEvent(
event);
case KEYCODE_DPAD_CENTER:
return mListener.onExitMoveMode() || super.dispatchKeyEvent(event);
default:
break;
}
}
return super.dispatchKeyEvent(event);
}
/**
* Shows user hints for moving the PiP, e.g. arrows.
*/
public void showMovementHints(int gravity) {
if (DEBUG) Log.d(TAG, "showMovementHints(), position: " + Gravity.toString(gravity));
animateAlphaTo((gravity & Gravity.BOTTOM) == Gravity.BOTTOM ? 1f : 0f, mArrowUp);
animateAlphaTo((gravity & Gravity.TOP) == Gravity.TOP ? 1f : 0f, mArrowDown);
animateAlphaTo((gravity & Gravity.RIGHT) == Gravity.RIGHT ? 1f : 0f, mArrowLeft);
animateAlphaTo((gravity & Gravity.LEFT) == Gravity.LEFT ? 1f : 0f, mArrowRight);
}
/**
* Hides user hints for moving the PiP, e.g. arrows.
*/
public void hideMovementHints() {
if (DEBUG) Log.d(TAG, "hideMovementHints()");
animateAlphaTo(0, mArrowUp);
animateAlphaTo(0, mArrowRight);
animateAlphaTo(0, mArrowDown);
animateAlphaTo(0, mArrowLeft);
}
/**
* Show or hide the pip user actions.
*/
public void showMenuButtons(boolean show) {
if (DEBUG) Log.d(TAG, "showMenuButtons: " + show);
animateAlphaTo(show ? 1 : 0, mActionButtonsContainer);
}
interface Listener {
void onBackPress();
void onEnterMoveMode();
/**
* @return whether move mode was exited
*/
boolean onExitMoveMode();
/**
* @return whether pip movement was handled.
*/
boolean onPipMovement(int keycode);
void onCloseButtonClick();
void onFullscreenButtonClick();
}
}

View File

@@ -29,7 +29,6 @@ import androidx.annotation.Nullable;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.pip.PipAnimationController;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
import com.android.wm.shell.pip.PipMenuController;
import com.android.wm.shell.pip.PipTransitionController;
@@ -42,11 +41,11 @@ import com.android.wm.shell.transition.Transitions;
public class TvPipTransition extends PipTransitionController {
public TvPipTransition(PipBoundsState pipBoundsState,
PipMenuController pipMenuController,
PipBoundsAlgorithm pipBoundsAlgorithm,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
PipAnimationController pipAnimationController,
Transitions transitions,
@NonNull ShellTaskOrganizer shellTaskOrganizer) {
super(pipBoundsState, pipMenuController, pipBoundsAlgorithm, pipAnimationController,
super(pipBoundsState, pipMenuController, tvPipBoundsAlgorithm, pipAnimationController,
transitions, shellTaskOrganizer);
}