1 /*
2  * Copyright (C) 2018 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.google.android.test.windowinsetstests;
18 
19 import static android.view.WindowInsets.Type.ime;
20 import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP;
21 import static java.lang.Math.max;
22 import static java.lang.Math.min;
23 
24 import android.animation.Animator;
25 import android.animation.AnimatorListenerAdapter;
26 import android.animation.ValueAnimator;
27 import android.annotation.NonNull;
28 import android.annotation.Nullable;
29 import android.content.Context;
30 import android.graphics.Insets;
31 import android.os.Bundle;
32 import android.util.AttributeSet;
33 import android.view.MotionEvent;
34 import android.view.View;
35 import android.view.ViewConfiguration;
36 import android.view.WindowInsets;
37 import android.view.WindowInsetsAnimation;
38 import android.view.WindowInsetsAnimation.Callback;
39 import android.view.WindowInsetsAnimationControlListener;
40 import android.view.WindowInsetsAnimationController;
41 import android.view.animation.LinearInterpolator;
42 import android.widget.LinearLayout;
43 
44 import java.util.ArrayList;
45 import java.util.List;
46 
47 import androidx.appcompat.app.AppCompatActivity;
48 
49 public class ChatActivity extends AppCompatActivity {
50 
51     private View mRoot;
52 
53     final ArrayList<Transition> mTransitions = new ArrayList<>();
54 
55     @Override
onCreate(Bundle savedInstanceState)56     protected void onCreate(Bundle savedInstanceState) {
57         super.onCreate(savedInstanceState);
58         setContentView(R.layout.chat_activity);
59 
60         setSupportActionBar(findViewById(R.id.toolbar));
61 
62         mRoot = findViewById(R.id.root);
63         mRoot.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
64                 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
65 
66         mTransitions.add(new Transition(findViewById(R.id.scrollView)));
67         mTransitions.add(new Transition(findViewById(R.id.editText)));
68 
69         mRoot.setOnTouchListener(new View.OnTouchListener() {
70             private final ViewConfiguration mViewConfiguration =
71                     ViewConfiguration.get(ChatActivity.this);
72             WindowInsetsAnimationController mAnimationController;
73             WindowInsetsAnimationControlListener mCurrentRequest;
74             boolean mRequestedController = false;
75             float mDown = 0;
76             float mCurrent = 0;
77             Insets mDownInsets = Insets.NONE;
78             boolean mShownAtDown;
79 
80             @Override
81             public boolean onTouch(View v, MotionEvent event) {
82                 mCurrent = event.getY();
83                 switch (event.getAction()) {
84                     case MotionEvent.ACTION_DOWN:
85                         mDown = event.getY();
86                         mDownInsets = v.getRootWindowInsets().getInsets(ime());
87                         mShownAtDown = v.getRootWindowInsets().isVisible(ime());
88                         mRequestedController = false;
89                         mCurrentRequest = null;
90                         break;
91                     case MotionEvent.ACTION_MOVE:
92                         if (mAnimationController != null) {
93                             updateInset();
94                         } else if (Math.abs(mDown - event.getY())
95                                 > mViewConfiguration.getScaledTouchSlop()
96                                 && !mRequestedController) {
97                             mRequestedController = true;
98                             v.getWindowInsetsController().controlWindowInsetsAnimation(ime(),
99                                     1000, new LinearInterpolator(), null /* cancellationSignal */,
100                                     mCurrentRequest = new WindowInsetsAnimationControlListener() {
101                                         @Override
102                                         public void onReady(
103                                                 @NonNull WindowInsetsAnimationController controller,
104                                                 int types) {
105                                             if (mCurrentRequest == this) {
106                                                 mAnimationController = controller;
107                                                 updateInset();
108                                             } else {
109                                                 controller.finish(mShownAtDown);
110                                             }
111                                         }
112 
113                                         @Override
114                                         public void onFinished(
115                                                 WindowInsetsAnimationController controller) {
116                                             mAnimationController = null;
117                                         }
118 
119                                         @Override
120                                         public void onCancelled(
121                                                 WindowInsetsAnimationController controller) {
122                                             mAnimationController = null;
123                                         }
124                                     });
125                         }
126                         break;
127                     case MotionEvent.ACTION_UP:
128                     case MotionEvent.ACTION_CANCEL:
129                         if (mAnimationController != null) {
130                             boolean isCancel = event.getAction() == MotionEvent.ACTION_CANCEL;
131                             mAnimationController.finish(isCancel ? mShownAtDown : !mShownAtDown);
132                             mAnimationController = null;
133                         }
134                         mRequestedController = false;
135                         mCurrentRequest = null;
136                         break;
137                 }
138                 return true;
139             }
140 
141             private void updateInset() {
142                 int inset = (int) (mDownInsets.bottom + (mDown - mCurrent));
143                 final int hidden = mAnimationController.getHiddenStateInsets().bottom;
144                 final int shown = mAnimationController.getShownStateInsets().bottom;
145                 final int start = mShownAtDown ? shown : hidden;
146                 final int end = mShownAtDown ? hidden : shown;
147                 inset = max(inset, hidden);
148                 inset = min(inset, shown);
149                 mAnimationController.setInsetsAndAlpha(
150                         Insets.of(0, 0, 0, inset),
151                         1f, (inset - start) / (float)(end - start));
152             }
153         });
154 
155         mRoot.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
156             @Override
157             public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
158                 mRoot.setPadding(insets.getSystemWindowInsetLeft(),
159                         insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(),
160                         insets.getSystemWindowInsetBottom());
161                 return WindowInsets.CONSUMED;
162             }
163         });
164 
165         mRoot.setWindowInsetsAnimationCallback(new Callback(DISPATCH_MODE_STOP) {
166 
167             @Override
168             public void onPrepare(WindowInsetsAnimation animation) {
169                 mTransitions.forEach(it -> it.onPrepare(animation));
170             }
171 
172             @Override
173             public WindowInsets onProgress(WindowInsets insets,
174                     @NonNull List<WindowInsetsAnimation> runningAnimations) {
175                 mTransitions.forEach(it -> it.onProgress(insets));
176                 return insets;
177             }
178 
179             @Override
180             public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation,
181                     WindowInsetsAnimation.Bounds bounds) {
182                 mTransitions.forEach(Transition::onStart);
183                 return bounds;
184             }
185 
186             @Override
187             public void onEnd(WindowInsetsAnimation animation) {
188                 mTransitions.forEach(it -> it.onFinish(animation));
189             }
190         });
191 
192         findViewById(R.id.floating_action_button).setOnClickListener(
193                 v -> v.getWindowInsetsController().controlWindowInsetsAnimation(ime(), -1,
194                 new LinearInterpolator(), null /* cancellationSignal */,
195                 new WindowInsetsAnimationControlListener() {
196                     @Override
197                     public void onReady(
198                             WindowInsetsAnimationController controller,
199                             int types) {
200                         ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
201                         anim.setDuration(1500);
202                         anim.addUpdateListener(animation
203                                 -> controller.setInsetsAndAlpha(
204                                 controller.getShownStateInsets(),
205                                 (float) animation.getAnimatedValue(),
206                                 anim.getAnimatedFraction()));
207                         anim.addListener(new AnimatorListenerAdapter() {
208                             @Override
209                             public void onAnimationEnd(Animator animation) {
210                                 super.onAnimationEnd(animation);
211                                 controller.finish(true);
212                             }
213                         });
214                         anim.start();
215                     }
216 
217                     @Override
218                     public void onCancelled(WindowInsetsAnimationController controller) {
219                     }
220 
221                     @Override
222                     public void onFinished(WindowInsetsAnimationController controller) {
223                     }
224                 }));
225     }
226 
227     @Override
onResume()228     public void onResume() {
229         super.onResume();
230         // TODO: move this to onCreate once setDecorFitsSystemWindows can be safely called there.
231         getWindow().getDecorView().post(() -> getWindow().setDecorFitsSystemWindows(false));
232     }
233 
234     static class Transition {
235         private int mEndBottom;
236         private int mStartBottom;
237         private final View mView;
238         private WindowInsetsAnimation mInsetsAnimation;
239 
Transition(View root)240         Transition(View root) {
241             mView = root;
242         }
243 
onPrepare(WindowInsetsAnimation animation)244         void onPrepare(WindowInsetsAnimation animation) {
245             if ((animation.getTypeMask() & ime()) != 0) {
246                 mInsetsAnimation = animation;
247             }
248             mStartBottom = mView.getBottom();
249         }
250 
onProgress(WindowInsets insets)251         void onProgress(WindowInsets insets) {
252             mView.setY(mStartBottom + (mEndBottom - mStartBottom)
253                     * mInsetsAnimation.getInterpolatedFraction()
254                     - mView.getHeight());
255         }
256 
onStart()257         void onStart() {
258             mEndBottom = mView.getBottom();
259         }
260 
onFinish(WindowInsetsAnimation animation)261         void onFinish(WindowInsetsAnimation animation) {
262             if (mInsetsAnimation == animation) {
263                 mInsetsAnimation = null;
264             }
265         }
266     }
267 
268     static class ImeLinearLayout extends LinearLayout {
269 
ImeLinearLayout(Context context, @Nullable AttributeSet attrs)270         public ImeLinearLayout(Context context,
271                 @Nullable AttributeSet attrs) {
272             super(context, attrs);
273         }
274     }
275 }
276