1 /*
2  * Copyright (C) 2023 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.wm.shell.compatui;
18 
19 import static android.app.TaskInfo.PROPERTY_VALUE_UNSET;
20 
21 import android.animation.Animator;
22 import android.animation.AnimatorListenerAdapter;
23 import android.animation.ObjectAnimator;
24 import android.animation.ValueAnimator;
25 import android.app.TaskInfo;
26 import android.content.Context;
27 import android.util.AttributeSet;
28 import android.view.View;
29 import android.widget.FrameLayout;
30 
31 import com.android.wm.shell.R;
32 
33 import java.util.function.BiConsumer;
34 import java.util.function.Function;
35 
36 /**
37  * Container for reachability education which handles all the show/hide animations.
38  */
39 public class ReachabilityEduLayout extends FrameLayout {
40 
41     private static final float ALPHA_FULL_TRANSPARENT = 0f;
42 
43     private static final float ALPHA_FULL_OPAQUE = 1f;
44 
45     private static final long VISIBILITY_ANIMATION_DURATION_MS = 400;
46 
47     private static final long MARGINS_ANIMATION_DURATION_MS = 250;
48 
49     private static final String ALPHA_PROPERTY_NAME = "alpha";
50 
51     private ReachabilityEduWindowManager mWindowManager;
52 
53     private View mMoveLeftButton;
54     private View mMoveRightButton;
55     private View mMoveUpButton;
56     private View mMoveDownButton;
57 
58     private int mLastLeftMargin = PROPERTY_VALUE_UNSET;
59     private int mLastRightMargin = PROPERTY_VALUE_UNSET;
60     private int mLastTopMargin = PROPERTY_VALUE_UNSET;
61     private int mLastBottomMargin = PROPERTY_VALUE_UNSET;
62 
ReachabilityEduLayout(Context context)63     public ReachabilityEduLayout(Context context) {
64         this(context, null);
65     }
66 
ReachabilityEduLayout(Context context, AttributeSet attrs)67     public ReachabilityEduLayout(Context context, AttributeSet attrs) {
68         this(context, attrs, 0);
69     }
70 
ReachabilityEduLayout(Context context, AttributeSet attrs, int defStyleAttr)71     public ReachabilityEduLayout(Context context, AttributeSet attrs, int defStyleAttr) {
72         this(context, attrs, defStyleAttr, 0);
73     }
74 
ReachabilityEduLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)75     public ReachabilityEduLayout(Context context, AttributeSet attrs, int defStyleAttr,
76             int defStyleRes) {
77         super(context, attrs, defStyleAttr, defStyleRes);
78     }
79 
inject(ReachabilityEduWindowManager windowManager)80     void inject(ReachabilityEduWindowManager windowManager) {
81         mWindowManager = windowManager;
82     }
83 
handleVisibility(boolean horizontalEnabled, boolean verticalEnabled, int letterboxVerticalPosition, int letterboxHorizontalPosition, int availableWidth, int availableHeight, CompatUIConfiguration compatUIConfiguration, TaskInfo taskInfo)84     void handleVisibility(boolean horizontalEnabled, boolean verticalEnabled,
85             int letterboxVerticalPosition,
86             int letterboxHorizontalPosition, int availableWidth, int availableHeight,
87             CompatUIConfiguration compatUIConfiguration, TaskInfo taskInfo) {
88         hideAllImmediately();
89         if (horizontalEnabled && letterboxHorizontalPosition != PROPERTY_VALUE_UNSET) {
90             handleLetterboxHorizontalPosition(availableWidth, letterboxHorizontalPosition);
91             compatUIConfiguration.setUserHasSeenHorizontalReachabilityEducation(taskInfo);
92         } else if (verticalEnabled && letterboxVerticalPosition != PROPERTY_VALUE_UNSET) {
93             handleLetterboxVerticalPosition(availableHeight, letterboxVerticalPosition);
94             compatUIConfiguration.setUserHasSeenVerticalReachabilityEducation(taskInfo);
95         }
96     }
97 
hideAllImmediately()98     void hideAllImmediately() {
99         hideImmediately(mMoveLeftButton);
100         hideImmediately(mMoveRightButton);
101         hideImmediately(mMoveUpButton);
102         hideImmediately(mMoveDownButton);
103         mLastLeftMargin = PROPERTY_VALUE_UNSET;
104         mLastRightMargin = PROPERTY_VALUE_UNSET;
105         mLastTopMargin = PROPERTY_VALUE_UNSET;
106         mLastBottomMargin = PROPERTY_VALUE_UNSET;
107     }
108 
109     @Override
onFinishInflate()110     protected void onFinishInflate() {
111         super.onFinishInflate();
112         mMoveLeftButton = findViewById(R.id.reachability_move_left_button);
113         mMoveRightButton = findViewById(R.id.reachability_move_right_button);
114         mMoveUpButton = findViewById(R.id.reachability_move_up_button);
115         mMoveDownButton = findViewById(R.id.reachability_move_down_button);
116         mMoveLeftButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
117         mMoveRightButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
118         mMoveUpButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
119         mMoveDownButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
120     }
121 
hideImmediately(View view)122     private void hideImmediately(View view) {
123         view.setAlpha(0);
124         view.setVisibility(View.INVISIBLE);
125     }
126 
marginAnimator(View view, Function<LayoutParams, Integer> marginSupplier, BiConsumer<LayoutParams, Integer> marginConsumer, int from, int to)127     private Animator marginAnimator(View view, Function<LayoutParams, Integer> marginSupplier,
128             BiConsumer<LayoutParams, Integer> marginConsumer, int from, int to) {
129         final LayoutParams layoutParams = ((LayoutParams) view.getLayoutParams());
130         ValueAnimator animator = ValueAnimator.ofInt(marginSupplier.apply(layoutParams), from, to);
131         animator.addUpdateListener(valueAnimator -> {
132             marginConsumer.accept(layoutParams, (Integer) valueAnimator.getAnimatedValue());
133             view.requestLayout();
134         });
135         animator.setDuration(MARGINS_ANIMATION_DURATION_MS);
136         return animator;
137     }
138 
handleLetterboxHorizontalPosition(int availableWidth, int letterboxHorizontalPosition)139     private void handleLetterboxHorizontalPosition(int availableWidth,
140             int letterboxHorizontalPosition) {
141         hideItem(mMoveUpButton);
142         hideItem(mMoveDownButton);
143         mLastTopMargin = PROPERTY_VALUE_UNSET;
144         mLastBottomMargin = PROPERTY_VALUE_UNSET;
145         // We calculate the available space on the left and right
146         final int horizontalGap = availableWidth / 2;
147         final int leftAvailableSpace = letterboxHorizontalPosition * horizontalGap;
148         final int rightAvailableSpace = availableWidth - leftAvailableSpace;
149         // We show the button if we have enough space
150         if (leftAvailableSpace >= mMoveLeftButton.getMeasuredWidth()) {
151             int newLeftMargin = (horizontalGap - mMoveLeftButton.getMeasuredWidth()) / 2;
152             if (mLastLeftMargin == PROPERTY_VALUE_UNSET) {
153                 mLastLeftMargin = newLeftMargin;
154             }
155             if (mLastLeftMargin != newLeftMargin) {
156                 marginAnimator(mMoveLeftButton, layoutParams -> layoutParams.leftMargin,
157                         (layoutParams, margin) -> layoutParams.leftMargin = margin,
158                         mLastLeftMargin, newLeftMargin).start();
159             } else {
160                 final LayoutParams leftParams = ((LayoutParams) mMoveLeftButton.getLayoutParams());
161                 leftParams.leftMargin = mLastLeftMargin;
162                 mMoveLeftButton.setLayoutParams(leftParams);
163             }
164             showItem(mMoveLeftButton);
165         } else {
166             hideItem(mMoveLeftButton);
167             mLastLeftMargin = PROPERTY_VALUE_UNSET;
168         }
169         if (rightAvailableSpace >= mMoveRightButton.getMeasuredWidth()) {
170             int newRightMargin = (horizontalGap - mMoveRightButton.getMeasuredWidth()) / 2;
171             if (mLastRightMargin == PROPERTY_VALUE_UNSET) {
172                 mLastRightMargin = newRightMargin;
173             }
174             if (mLastRightMargin != newRightMargin) {
175                 marginAnimator(mMoveRightButton, layoutParams -> layoutParams.rightMargin,
176                         (layoutParams, margin) -> layoutParams.rightMargin = margin,
177                         mLastRightMargin, newRightMargin).start();
178             } else {
179                 final LayoutParams rightParams =
180                         ((LayoutParams) mMoveRightButton.getLayoutParams());
181                 rightParams.rightMargin = mLastRightMargin;
182                 mMoveRightButton.setLayoutParams(rightParams);
183             }
184             showItem(mMoveRightButton);
185         } else {
186             hideItem(mMoveRightButton);
187             mLastRightMargin = PROPERTY_VALUE_UNSET;
188         }
189     }
190 
handleLetterboxVerticalPosition(int availableHeight, int letterboxVerticalPosition)191     private void handleLetterboxVerticalPosition(int availableHeight,
192             int letterboxVerticalPosition) {
193         hideItem(mMoveLeftButton);
194         hideItem(mMoveRightButton);
195         mLastLeftMargin = PROPERTY_VALUE_UNSET;
196         mLastRightMargin = PROPERTY_VALUE_UNSET;
197         // We calculate the available space on the left and right
198         final int verticalGap = availableHeight / 2;
199         final int topAvailableSpace = letterboxVerticalPosition * verticalGap;
200         final int bottomAvailableSpace = availableHeight - topAvailableSpace;
201         if (topAvailableSpace >= mMoveUpButton.getMeasuredHeight()) {
202             int newTopMargin = (verticalGap - mMoveUpButton.getMeasuredHeight()) / 2;
203             if (mLastTopMargin == PROPERTY_VALUE_UNSET) {
204                 mLastTopMargin = newTopMargin;
205             }
206             if (mLastTopMargin != newTopMargin) {
207                 marginAnimator(mMoveUpButton, layoutParams -> layoutParams.topMargin,
208                         (layoutParams, margin) -> layoutParams.topMargin = margin,
209                         mLastTopMargin, newTopMargin).start();
210             } else {
211                 final LayoutParams topParams = ((LayoutParams) mMoveUpButton.getLayoutParams());
212                 topParams.topMargin = mLastTopMargin;
213                 mMoveUpButton.setLayoutParams(topParams);
214             }
215             showItem(mMoveUpButton);
216         } else {
217             hideItem(mMoveUpButton);
218             mLastTopMargin = PROPERTY_VALUE_UNSET;
219         }
220         if (bottomAvailableSpace >= mMoveDownButton.getMeasuredHeight()) {
221             int newBottomMargin = (verticalGap - mMoveDownButton.getMeasuredHeight()) / 2;
222             if (mLastBottomMargin == PROPERTY_VALUE_UNSET) {
223                 mLastBottomMargin = newBottomMargin;
224             }
225             if (mLastBottomMargin != newBottomMargin) {
226                 marginAnimator(mMoveDownButton, layoutParams -> layoutParams.bottomMargin,
227                         (layoutParams, margin) -> layoutParams.bottomMargin = margin,
228                         mLastBottomMargin, newBottomMargin).start();
229             } else {
230                 final LayoutParams bottomParams =
231                         ((LayoutParams) mMoveDownButton.getLayoutParams());
232                 bottomParams.bottomMargin = mLastBottomMargin;
233                 mMoveDownButton.setLayoutParams(bottomParams);
234             }
235             showItem(mMoveDownButton);
236         } else {
237             hideItem(mMoveDownButton);
238             mLastBottomMargin = PROPERTY_VALUE_UNSET;
239         }
240     }
241 
showItem(View view)242     private void showItem(View view) {
243         view.setVisibility(View.VISIBLE);
244         ObjectAnimator fadeIn = ObjectAnimator.ofFloat(view, ALPHA_PROPERTY_NAME,
245                 ALPHA_FULL_TRANSPARENT, ALPHA_FULL_OPAQUE);
246         fadeIn.setDuration(VISIBILITY_ANIMATION_DURATION_MS);
247         fadeIn.addListener(new AnimatorListenerAdapter() {
248             @Override
249             public void onAnimationEnd(Animator animation) {
250                 view.setVisibility(View.VISIBLE);
251             }
252         });
253         fadeIn.start();
254     }
255 
hideItem(View view)256     private void hideItem(View view) {
257         ObjectAnimator fadeOut = ObjectAnimator.ofFloat(view, ALPHA_PROPERTY_NAME,
258                 ALPHA_FULL_OPAQUE, ALPHA_FULL_TRANSPARENT);
259         fadeOut.setDuration(VISIBILITY_ANIMATION_DURATION_MS);
260         fadeOut.addListener(new AnimatorListenerAdapter() {
261             @Override
262             public void onAnimationEnd(Animator animation) {
263                 view.setVisibility(View.INVISIBLE);
264             }
265         });
266         fadeOut.start();
267     }
268 
269 }
270