| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.google.android.test.windowinsetstests; |
| |
| import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; |
| |
| import static java.lang.Math.max; |
| import static java.lang.Math.min; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.graphics.Insets; |
| import android.os.Bundle; |
| import android.util.AttributeSet; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.Window; |
| import android.view.WindowInsets; |
| import android.view.WindowInsets.Type; |
| import android.view.WindowInsetsAnimation; |
| import android.view.WindowInsetsAnimation.Callback; |
| import android.view.WindowInsetsAnimationControlListener; |
| import android.view.WindowInsetsAnimationController; |
| import android.view.animation.LinearInterpolator; |
| import android.widget.LinearLayout; |
| |
| import androidx.appcompat.app.AppCompatActivity; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| public class WindowInsetsActivity extends AppCompatActivity { |
| |
| private View mRoot; |
| |
| final ArrayList<Transition> mTransitions = new ArrayList<>(); |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| setContentView(R.layout.window_inset_activity); |
| |
| setSupportActionBar(findViewById(R.id.toolbar)); |
| |
| mRoot = findViewById(R.id.root); |
| mRoot.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
| | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); |
| |
| mTransitions.add(new Transition(findViewById(R.id.scrollView))); |
| mTransitions.add(new Transition(findViewById(R.id.editText))); |
| |
| mRoot.setOnTouchListener(new View.OnTouchListener() { |
| private final ViewConfiguration mViewConfiguration = |
| ViewConfiguration.get(WindowInsetsActivity.this); |
| WindowInsetsAnimationController mAnimationController; |
| WindowInsetsAnimationControlListener mCurrentRequest; |
| boolean mRequestedController = false; |
| float mDown = 0; |
| float mCurrent = 0; |
| Insets mDownInsets = Insets.NONE; |
| boolean mShownAtDown; |
| |
| @Override |
| public boolean onTouch(View v, MotionEvent event) { |
| mCurrent = event.getY(); |
| switch (event.getAction()) { |
| case MotionEvent.ACTION_DOWN: |
| mDown = event.getY(); |
| mDownInsets = v.getRootWindowInsets().getInsets(Type.ime()); |
| mShownAtDown = v.getRootWindowInsets().isVisible(Type.ime()); |
| mRequestedController = false; |
| mCurrentRequest = null; |
| break; |
| case MotionEvent.ACTION_MOVE: |
| if (mAnimationController != null) { |
| updateInset(); |
| } else if (Math.abs(mDown - event.getY()) |
| > mViewConfiguration.getScaledTouchSlop() |
| && !mRequestedController) { |
| mRequestedController = true; |
| v.getWindowInsetsController().controlWindowInsetsAnimation(Type.ime(), |
| 1000, new LinearInterpolator(), |
| mCurrentRequest = new WindowInsetsAnimationControlListener() { |
| @Override |
| public void onReady( |
| @NonNull WindowInsetsAnimationController controller, |
| int types) { |
| if (mCurrentRequest == this) { |
| mAnimationController = controller; |
| updateInset(); |
| } else { |
| controller.finish(mShownAtDown); |
| } |
| } |
| |
| @Override |
| public void onCancelled() { |
| mAnimationController = null; |
| } |
| }); |
| } |
| break; |
| case MotionEvent.ACTION_UP: |
| case MotionEvent.ACTION_CANCEL: |
| if (mAnimationController != null) { |
| boolean isCancel = event.getAction() == MotionEvent.ACTION_CANCEL; |
| mAnimationController.finish(isCancel ? mShownAtDown : !mShownAtDown); |
| mAnimationController = null; |
| } |
| mRequestedController = false; |
| mCurrentRequest = null; |
| break; |
| } |
| return true; |
| } |
| |
| private void updateInset() { |
| int inset = (int) (mDownInsets.bottom + (mDown - mCurrent)); |
| final int hidden = mAnimationController.getHiddenStateInsets().bottom; |
| final int shown = mAnimationController.getShownStateInsets().bottom; |
| final int start = mShownAtDown ? shown : hidden; |
| final int end = mShownAtDown ? hidden : shown; |
| inset = max(inset, hidden); |
| inset = min(inset, shown); |
| mAnimationController.setInsetsAndAlpha( |
| Insets.of(0, 0, 0, inset), |
| 1f, (inset - start) / (float)(end - start)); |
| } |
| }); |
| |
| mRoot.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { |
| @Override |
| public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { |
| mRoot.setPadding(insets.getSystemWindowInsetLeft(), |
| insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), |
| insets.getSystemWindowInsetBottom()); |
| return WindowInsets.CONSUMED; |
| } |
| }); |
| |
| mRoot.setWindowInsetsAnimationCallback(new Callback(DISPATCH_MODE_STOP) { |
| |
| @Override |
| public void onPrepare(WindowInsetsAnimation animation) { |
| mTransitions.forEach(it -> it.onPrepare(animation)); |
| } |
| |
| @Override |
| public WindowInsets onProgress(WindowInsets insets, |
| @NonNull List<WindowInsetsAnimation> runningAnimations) { |
| mTransitions.forEach(it -> it.onProgress(insets)); |
| return insets; |
| } |
| |
| @Override |
| public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation, |
| WindowInsetsAnimation.Bounds bounds) { |
| mTransitions.forEach(Transition::onStart); |
| return bounds; |
| } |
| |
| @Override |
| public void onEnd(WindowInsetsAnimation animation) { |
| mTransitions.forEach(it -> it.onFinish(animation)); |
| } |
| }); |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| // TODO: move this to onCreate once setDecorFitsSystemWindows can be safely called there. |
| getWindow().getDecorView().post(() -> getWindow().setDecorFitsSystemWindows(false)); |
| } |
| |
| static class Transition { |
| private int mEndBottom; |
| private int mStartBottom; |
| private final View mView; |
| private WindowInsetsAnimation mInsetsAnimation; |
| |
| Transition(View root) { |
| mView = root; |
| } |
| |
| void onPrepare(WindowInsetsAnimation animation) { |
| if ((animation.getTypeMask() & Type.ime()) != 0) { |
| mInsetsAnimation = animation; |
| } |
| mStartBottom = mView.getBottom(); |
| } |
| |
| void onProgress(WindowInsets insets) { |
| mView.setY(mStartBottom + (mEndBottom - mStartBottom) |
| * mInsetsAnimation.getInterpolatedFraction() |
| - mView.getHeight()); |
| } |
| |
| void onStart() { |
| mEndBottom = mView.getBottom(); |
| } |
| |
| void onFinish(WindowInsetsAnimation animation) { |
| if (mInsetsAnimation == animation) { |
| mInsetsAnimation = null; |
| } |
| } |
| } |
| |
| static class ImeLinearLayout extends LinearLayout { |
| |
| public ImeLinearLayout(Context context, |
| @Nullable AttributeSet attrs) { |
| super(context, attrs); |
| } |
| } |
| } |