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.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON; 20 import static com.android.settingslib.widget.theme.R.dimen.settingslib_preferred_minimum_touch_target; 21 22 import android.animation.Animator; 23 import android.animation.AnimatorListenerAdapter; 24 import android.content.Context; 25 import android.graphics.Rect; 26 import android.util.AttributeSet; 27 import android.util.FloatProperty; 28 import android.view.TouchDelegate; 29 import android.view.ViewGroup; 30 import android.widget.LinearLayout; 31 import android.widget.TextView; 32 33 import androidx.annotation.Nullable; 34 import androidx.dynamicanimation.animation.DynamicAnimation; 35 import androidx.dynamicanimation.animation.SpringAnimation; 36 import androidx.dynamicanimation.animation.SpringForce; 37 38 import com.android.app.animation.Interpolators; 39 import com.android.launcher3.R; 40 import com.android.launcher3.Utilities; 41 import com.android.launcher3.anim.PendingAnimation; 42 import com.android.launcher3.statemanager.StateManager; 43 import com.android.quickstep.util.AnimUtils; 44 import com.android.quickstep.util.SplitSelectStateController; 45 import com.android.wm.shell.shared.TypefaceUtils; 46 import com.android.wm.shell.shared.TypefaceUtils.FontFamily; 47 48 /** 49 * A rounded rectangular component containing a single TextView. 50 * Appears when a split is in progress, and tells the user to select a second app to initiate 51 * splitscreen. 52 * 53 * Appears and disappears concurrently with a FloatingTaskView. 54 */ 55 public class SplitInstructionsView extends LinearLayout { 56 private static final int BOUNCE_DURATION = 250; 57 private static final float BOUNCE_HEIGHT = 20; 58 59 private final RecentsViewContainer mContainer; 60 public boolean mIsCurrentlyAnimating = false; 61 62 public static final FloatProperty<SplitInstructionsView> UNFOLD = 63 new FloatProperty<>("SplitInstructionsUnfold") { 64 @Override 65 public void setValue(SplitInstructionsView splitInstructionsView, float v) { 66 splitInstructionsView.setScaleY(v); 67 } 68 69 @Override 70 public Float get(SplitInstructionsView splitInstructionsView) { 71 return splitInstructionsView.getScaleY(); 72 } 73 }; 74 75 public static final FloatProperty<SplitInstructionsView> TRANSLATE_Y = 76 new FloatProperty<>("SplitInstructionsTranslateY") { 77 @Override 78 public void setValue(SplitInstructionsView splitInstructionsView, float v) { 79 splitInstructionsView.setTranslationY(v); 80 } 81 82 @Override 83 public Float get(SplitInstructionsView splitInstructionsView) { 84 return splitInstructionsView.getTranslationY(); 85 } 86 }; 87 SplitInstructionsView(Context context)88 public SplitInstructionsView(Context context) { 89 this(context, null); 90 } 91 SplitInstructionsView(Context context, @Nullable AttributeSet attrs)92 public SplitInstructionsView(Context context, @Nullable AttributeSet attrs) { 93 this(context, attrs, 0); 94 } 95 SplitInstructionsView(Context context, AttributeSet attrs, int defStyleAttr)96 public SplitInstructionsView(Context context, AttributeSet attrs, int defStyleAttr) { 97 super(context, attrs, defStyleAttr); 98 mContainer = RecentsViewContainer.containerFromContext(context); 99 } 100 getSplitInstructionsView(RecentsViewContainer container)101 public static SplitInstructionsView getSplitInstructionsView(RecentsViewContainer container) { 102 ViewGroup dragLayer = container.getDragLayer(); 103 final SplitInstructionsView splitInstructionsView = 104 (SplitInstructionsView) container.getLayoutInflater().inflate( 105 R.layout.split_instructions_view, 106 dragLayer, 107 false 108 ); 109 splitInstructionsView.init(); 110 111 // Since textview overlays base view, and we sometimes manipulate the alpha of each 112 // simultaneously, force overlapping rendering to false prevents redrawing of pixels, 113 // improving performance at the cost of some accuracy. 114 splitInstructionsView.forceHasOverlappingRendering(false); 115 116 dragLayer.addView(splitInstructionsView); 117 return splitInstructionsView; 118 } 119 120 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)121 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 122 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 123 ensureProperRotation(); 124 } 125 init()126 private void init() { 127 TextView cancelTextView = findViewById(R.id.split_instructions_text_cancel); 128 TextView instructionTextView = findViewById(R.id.split_instructions_text); 129 130 cancelTextView.setVisibility(VISIBLE); 131 cancelTextView.setOnClickListener((v) -> exitSplitSelection()); 132 instructionTextView.setText(R.string.toast_contextual_split_select_app); 133 TypefaceUtils.setTypeface(instructionTextView, FontFamily.GSF_BODY_MEDIUM); 134 135 // After layout, expand touch target of cancel button to meet minimum a11y measurements. 136 post(() -> { 137 int minTouchSize = getResources() 138 .getDimensionPixelSize(settingslib_preferred_minimum_touch_target); 139 Rect r = new Rect(); 140 cancelTextView.getHitRect(r); 141 142 if (r.width() < minTouchSize) { 143 // add 1 to ensure ceiling on int division 144 int expandAmount = (minTouchSize + 1 - r.width()) / 2; 145 r.left -= expandAmount; 146 r.right += expandAmount; 147 } 148 if (r.height() < minTouchSize) { 149 int expandAmount = (minTouchSize + 1 - r.height()) / 2; 150 r.top -= expandAmount; 151 r.bottom += expandAmount; 152 } 153 154 setTouchDelegate(new TouchDelegate(r, cancelTextView)); 155 }); 156 157 // Set accessibility title, will be announced by a11y tools. 158 instructionTextView.setAccessibilityPaneTitle(instructionTextView.getText()); 159 } 160 exitSplitSelection()161 private void exitSplitSelection() { 162 RecentsView recentsView = mContainer.getOverviewPanel(); 163 SplitSelectStateController splitSelectController = recentsView.getSplitSelectController(); 164 StateManager stateManager = recentsView.getStateManager(); 165 166 AnimUtils.goToNormalStateWithSplitDismissal(stateManager, mContainer, 167 LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON, 168 splitSelectController.getSplitAnimationController()); 169 } 170 ensureProperRotation()171 void ensureProperRotation() { 172 ((RecentsView) mContainer.getOverviewPanel()).getPagedOrientationHandler() 173 .setSplitInstructionsParams( 174 this, 175 mContainer.getDeviceProfile(), 176 getMeasuredHeight(), 177 getMeasuredWidth() 178 ); 179 } 180 181 /** 182 * Draws attention to the split instructions view by bouncing it up and down. 183 */ goBoing()184 public void goBoing() { 185 if (mIsCurrentlyAnimating) { 186 return; 187 } 188 189 float restingY = getTranslationY(); 190 float bounceToY = restingY - Utilities.dpToPx(BOUNCE_HEIGHT); 191 PendingAnimation anim = new PendingAnimation(BOUNCE_DURATION); 192 // Animate the view lifting up to a higher position 193 anim.addFloat(this, TRANSLATE_Y, restingY, bounceToY, Interpolators.STANDARD); 194 195 anim.addListener(new AnimatorListenerAdapter() { 196 @Override 197 public void onAnimationStart(Animator animation) { 198 mIsCurrentlyAnimating = true; 199 } 200 201 @Override 202 public void onAnimationEnd(Animator animation) { 203 // Create a low stiffness, medium bounce spring centering at the rest position 204 SpringForce spring = new SpringForce(restingY) 205 .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) 206 .setStiffness(SpringForce.STIFFNESS_LOW); 207 // Animate the view getting pulled back to rest position by the spring 208 SpringAnimation springAnim = new SpringAnimation(SplitInstructionsView.this, 209 DynamicAnimation.TRANSLATION_Y).setSpring(spring).setStartValue(bounceToY); 210 211 springAnim.addEndListener((a, b, c, d) -> mIsCurrentlyAnimating = false); 212 springAnim.start(); 213 } 214 }); 215 216 anim.buildAnim().start(); 217 } 218 } 219