1 /* 2 * Copyright 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.quickstep.views; 18 19 import static com.android.launcher3.LauncherState.NORMAL; 20 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON; 21 import static com.android.settingslib.widget.theme.R.dimen.settingslib_preferred_minimum_touch_target; 22 23 import android.animation.Animator; 24 import android.animation.AnimatorListenerAdapter; 25 import android.animation.AnimatorSet; 26 import android.content.Context; 27 import android.graphics.Rect; 28 import android.util.AttributeSet; 29 import android.util.FloatProperty; 30 import android.view.TouchDelegate; 31 import android.view.ViewGroup; 32 import android.widget.LinearLayout; 33 import android.widget.TextView; 34 35 import androidx.annotation.Nullable; 36 import androidx.dynamicanimation.animation.DynamicAnimation; 37 import androidx.dynamicanimation.animation.SpringAnimation; 38 import androidx.dynamicanimation.animation.SpringForce; 39 40 import com.android.app.animation.Interpolators; 41 import com.android.launcher3.R; 42 import com.android.launcher3.Utilities; 43 import com.android.launcher3.anim.PendingAnimation; 44 import com.android.launcher3.config.FeatureFlags; 45 import com.android.launcher3.statemanager.BaseState; 46 import com.android.launcher3.statemanager.StateManager; 47 import com.android.launcher3.states.StateAnimationConfig; 48 import com.android.quickstep.util.SplitSelectStateController; 49 50 /** 51 * A rounded rectangular component containing a single TextView. 52 * Appears when a split is in progress, and tells the user to select a second app to initiate 53 * splitscreen. 54 * 55 * Appears and disappears concurrently with a FloatingTaskView. 56 */ 57 public class SplitInstructionsView extends LinearLayout { 58 private static final int BOUNCE_DURATION = 250; 59 private static final float BOUNCE_HEIGHT = 20; 60 private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350; 61 62 private final RecentsViewContainer mContainer; 63 public boolean mIsCurrentlyAnimating = false; 64 65 public static final FloatProperty<SplitInstructionsView> UNFOLD = 66 new FloatProperty<>("SplitInstructionsUnfold") { 67 @Override 68 public void setValue(SplitInstructionsView splitInstructionsView, float v) { 69 splitInstructionsView.setScaleY(v); 70 } 71 72 @Override 73 public Float get(SplitInstructionsView splitInstructionsView) { 74 return splitInstructionsView.getScaleY(); 75 } 76 }; 77 78 public static final FloatProperty<SplitInstructionsView> TRANSLATE_Y = 79 new FloatProperty<>("SplitInstructionsTranslateY") { 80 @Override 81 public void setValue(SplitInstructionsView splitInstructionsView, float v) { 82 splitInstructionsView.setTranslationY(v); 83 } 84 85 @Override 86 public Float get(SplitInstructionsView splitInstructionsView) { 87 return splitInstructionsView.getTranslationY(); 88 } 89 }; 90 SplitInstructionsView(Context context)91 public SplitInstructionsView(Context context) { 92 this(context, null); 93 } 94 SplitInstructionsView(Context context, @Nullable AttributeSet attrs)95 public SplitInstructionsView(Context context, @Nullable AttributeSet attrs) { 96 this(context, attrs, 0); 97 } 98 SplitInstructionsView(Context context, AttributeSet attrs, int defStyleAttr)99 public SplitInstructionsView(Context context, AttributeSet attrs, int defStyleAttr) { 100 super(context, attrs, defStyleAttr); 101 mContainer = RecentsViewContainer.containerFromContext(context); 102 } 103 getSplitInstructionsView(RecentsViewContainer container)104 public static SplitInstructionsView getSplitInstructionsView(RecentsViewContainer container) { 105 ViewGroup dragLayer = container.getDragLayer(); 106 final SplitInstructionsView splitInstructionsView = 107 (SplitInstructionsView) container.getLayoutInflater().inflate( 108 R.layout.split_instructions_view, 109 dragLayer, 110 false 111 ); 112 splitInstructionsView.init(); 113 114 // Since textview overlays base view, and we sometimes manipulate the alpha of each 115 // simultaneously, force overlapping rendering to false prevents redrawing of pixels, 116 // improving performance at the cost of some accuracy. 117 splitInstructionsView.forceHasOverlappingRendering(false); 118 119 dragLayer.addView(splitInstructionsView); 120 return splitInstructionsView; 121 } 122 123 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)124 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 125 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 126 ensureProperRotation(); 127 } 128 init()129 private void init() { 130 TextView cancelTextView = findViewById(R.id.split_instructions_text_cancel); 131 TextView instructionTextView = findViewById(R.id.split_instructions_text); 132 133 if (FeatureFlags.enableSplitContextually()) { 134 cancelTextView.setVisibility(VISIBLE); 135 cancelTextView.setOnClickListener((v) -> exitSplitSelection()); 136 instructionTextView.setText(R.string.toast_contextual_split_select_app); 137 138 // After layout, expand touch target of cancel button to meet minimum a11y measurements. 139 post(() -> { 140 int minTouchSize = getResources() 141 .getDimensionPixelSize(settingslib_preferred_minimum_touch_target); 142 Rect r = new Rect(); 143 cancelTextView.getHitRect(r); 144 145 if (r.width() < minTouchSize) { 146 // add 1 to ensure ceiling on int division 147 int expandAmount = (minTouchSize + 1 - r.width()) / 2; 148 r.left -= expandAmount; 149 r.right += expandAmount; 150 } 151 if (r.height() < minTouchSize) { 152 int expandAmount = (minTouchSize + 1 - r.height()) / 2; 153 r.top -= expandAmount; 154 r.bottom += expandAmount; 155 } 156 157 setTouchDelegate(new TouchDelegate(r, cancelTextView)); 158 }); 159 } 160 161 // Set accessibility title, will be announced by a11y tools. 162 instructionTextView.setAccessibilityPaneTitle(instructionTextView.getText()); 163 } 164 exitSplitSelection()165 private void exitSplitSelection() { 166 RecentsView recentsView = mContainer.getOverviewPanel(); 167 SplitSelectStateController splitSelectController = recentsView.getSplitSelectController(); 168 169 StateManager stateManager = recentsView.getStateManager(); 170 BaseState startState = stateManager.getState(); 171 long duration = startState.getTransitionDuration(mContainer.asContext(), false); 172 if (duration == 0) { 173 // Case where we're in contextual on workspace (NORMAL), which by default has 0 174 // transition duration 175 duration = DURATION_DEFAULT_SPLIT_DISMISS; 176 } 177 StateAnimationConfig config = new StateAnimationConfig(); 178 config.duration = duration; 179 AnimatorSet stateAnim = stateManager.createAtomicAnimation( 180 startState, NORMAL, config); 181 AnimatorSet dismissAnim = splitSelectController.getSplitAnimationController() 182 .createPlaceholderDismissAnim(mContainer, 183 LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON, duration); 184 stateAnim.play(dismissAnim); 185 stateManager.setCurrentAnimation(stateAnim, NORMAL); 186 stateAnim.start(); 187 } 188 ensureProperRotation()189 void ensureProperRotation() { 190 ((RecentsView) mContainer.getOverviewPanel()).getPagedOrientationHandler() 191 .setSplitInstructionsParams( 192 this, 193 mContainer.getDeviceProfile(), 194 getMeasuredHeight(), 195 getMeasuredWidth() 196 ); 197 } 198 199 /** 200 * Draws attention to the split instructions view by bouncing it up and down. 201 */ goBoing()202 public void goBoing() { 203 if (mIsCurrentlyAnimating) { 204 return; 205 } 206 207 float restingY = getTranslationY(); 208 float bounceToY = restingY - Utilities.dpToPx(BOUNCE_HEIGHT); 209 PendingAnimation anim = new PendingAnimation(BOUNCE_DURATION); 210 // Animate the view lifting up to a higher position 211 anim.addFloat(this, TRANSLATE_Y, restingY, bounceToY, Interpolators.STANDARD); 212 213 anim.addListener(new AnimatorListenerAdapter() { 214 @Override 215 public void onAnimationStart(Animator animation) { 216 mIsCurrentlyAnimating = true; 217 } 218 219 @Override 220 public void onAnimationEnd(Animator animation) { 221 // Create a low stiffness, medium bounce spring centering at the rest position 222 SpringForce spring = new SpringForce(restingY) 223 .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) 224 .setStiffness(SpringForce.STIFFNESS_LOW); 225 // Animate the view getting pulled back to rest position by the spring 226 SpringAnimation springAnim = new SpringAnimation(SplitInstructionsView.this, 227 DynamicAnimation.TRANSLATION_Y).setSpring(spring).setStartValue(bounceToY); 228 229 springAnim.addEndListener((a, b, c, d) -> mIsCurrentlyAnimating = false); 230 springAnim.start(); 231 } 232 }); 233 234 anim.buildAnim().start(); 235 } 236 } 237