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