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