diff options
11 files changed, 432 insertions, 84 deletions
diff --git a/tests/WindowInsetsTests/Android.bp b/tests/WindowInsetsTests/Android.bp index 12395e70468d..7272152dc257 100644 --- a/tests/WindowInsetsTests/Android.bp +++ b/tests/WindowInsetsTests/Android.bp @@ -18,5 +18,10 @@ android_test { resource_dirs: ["res"], certificate: "platform", platform_apis: true, + static_libs: [ + "androidx.core_core", + "androidx.appcompat_appcompat", + "com.google.android.material_material", + ], } diff --git a/tests/WindowInsetsTests/AndroidManifest.xml b/tests/WindowInsetsTests/AndroidManifest.xml index 8d33f70c33a2..0f6282e20b41 100644 --- a/tests/WindowInsetsTests/AndroidManifest.xml +++ b/tests/WindowInsetsTests/AndroidManifest.xml @@ -20,7 +20,7 @@ <application android:label="@string/activity_title"> <activity android:name=".WindowInsetsActivity" - android:theme="@android:style/Theme.Material" + android:theme="@style/appTheme" android:windowSoftInputMode="adjustResize"> <intent-filter> diff --git a/tests/WindowInsetsTests/res/drawable/bubble.xml b/tests/WindowInsetsTests/res/drawable/bubble.xml new file mode 100644 index 000000000000..26deb1e59e41 --- /dev/null +++ b/tests/WindowInsetsTests/res/drawable/bubble.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@color/bubble" /> + <corners android:radius="@dimen/bubble_corner" /> + <padding android:left="@dimen/bubble_padding_side" android:top="@dimen/bubble_padding" + android:right="@dimen/bubble_padding_side" android:bottom="@dimen/bubble_padding" /> +</shape>
\ No newline at end of file diff --git a/tests/WindowInsetsTests/res/drawable/bubble_self.xml b/tests/WindowInsetsTests/res/drawable/bubble_self.xml new file mode 100644 index 000000000000..5f098a2fea60 --- /dev/null +++ b/tests/WindowInsetsTests/res/drawable/bubble_self.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@color/bubble_self" /> + <corners android:radius="@dimen/bubble_corner" /> + <padding android:left="@dimen/bubble_padding_side" android:top="@dimen/bubble_padding" + android:right="@dimen/bubble_padding_side" android:bottom="@dimen/bubble_padding" /> +</shape>
\ No newline at end of file diff --git a/tests/WindowInsetsTests/res/drawable/ic_send.xml b/tests/WindowInsetsTests/res/drawable/ic_send.xml new file mode 100644 index 000000000000..15bc411e85fb --- /dev/null +++ b/tests/WindowInsetsTests/res/drawable/ic_send.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:autoMirrored="true" + android:width="24.0dp" + android:height="24.0dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path + android:fillColor="#FF000000" + android:pathData="M4.02,42.0L46.0,24.0 4.02,6.0 4.0,20.0l30.0,4.0 -30.0,4.0z"/> +</vector> diff --git a/tests/WindowInsetsTests/res/layout/message.xml b/tests/WindowInsetsTests/res/layout/message.xml new file mode 100644 index 000000000000..d6b29c33f662 --- /dev/null +++ b/tests/WindowInsetsTests/res/layout/message.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> + +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/bubble" + android:textSize="32sp" + android:text="Hello World"> + +</TextView>
\ No newline at end of file diff --git a/tests/WindowInsetsTests/res/layout/message_self.xml b/tests/WindowInsetsTests/res/layout/message_self.xml new file mode 100644 index 000000000000..de34e4896331 --- /dev/null +++ b/tests/WindowInsetsTests/res/layout/message_self.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> + +<merge> + <include + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt" + layout="@layout/message"> + <aapt:attr name="android:theme"> + <style> + <item name="android:layout_gravity">end</item> + <item name="bubbleBackground">@color/bubble_self</item> + </style> + </aapt:attr> + </include> +</merge> diff --git a/tests/WindowInsetsTests/res/layout/window_inset_activity.xml b/tests/WindowInsetsTests/res/layout/window_inset_activity.xml index 38e00298f49f..1b51c4f83fe0 100644 --- a/tests/WindowInsetsTests/res/layout/window_inset_activity.xml +++ b/tests/WindowInsetsTests/res/layout/window_inset_activity.xml @@ -17,17 +17,82 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" - android:gravity="center" + android:clipToPadding="false" android:id="@+id/root"> - <Button - android:id="@+id/button" - android:layout_width="wrap_content" - android:layout_height="48dp" - android:text="Hello insets" /> + <androidx.appcompat.widget.Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + /> + <FrameLayout + android:id="@+id/scrollView" + android:layout_height="0dp" + android:layout_width="match_parent" + android:paddingStart="8dp" + android:paddingEnd="8dp" + android:layout_weight="1"> + + <LinearLayout + android:orientation="vertical" + android:layout_gravity="bottom" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="@style/bubble" + android:text="Hey, look at this buttery smooth animation!" /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="@style/bubble_self" + android:text="Wow, that's pretty neat, how does this work?" /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="@style/bubble" + android:text="Using the new WindowInsets animation system of course!" /> + + </LinearLayout> + + </FrameLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingStart="8dp" + android:paddingEnd="8dp" + android:id="@+id/editText"> + + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1"> + + <com.google.android.material.textfield.TextInputEditText + android:hint="Text message" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/floating_action_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + app:elevation="0dp" + app:fabSize="mini" + app:srcCompat="@drawable/ic_send"/> + + </LinearLayout> </LinearLayout> diff --git a/tests/WindowInsetsTests/res/values/strings.xml b/tests/WindowInsetsTests/res/values/strings.xml index 242823d06fc8..2b8e5f3da362 100644 --- a/tests/WindowInsetsTests/res/values/strings.xml +++ b/tests/WindowInsetsTests/res/values/strings.xml @@ -16,5 +16,5 @@ --> <resources> - <string name="activity_title">Window Insets Tests</string> + <string name="activity_title">New Insets Chat</string> </resources> diff --git a/tests/WindowInsetsTests/res/values/styles.xml b/tests/WindowInsetsTests/res/values/styles.xml new file mode 100644 index 000000000000..220671fb8e71 --- /dev/null +++ b/tests/WindowInsetsTests/res/values/styles.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> + +<resources> + + <style name="appTheme" parent="@style/Theme.MaterialComponents.Light"> + <item name="windowActionBar">false</item> + <item name="windowNoTitle">true</item> + + <item name="colorPrimary">@color/primaryColor</item> + <item name="colorPrimaryDark">@color/primaryDarkColor</item> + <item name="colorSecondary">?attr/colorPrimary</item> + <item name="colorOnSecondary">@color/primaryTextColor</item> + + <!-- Window decor --> + <item name="android:statusBarColor">#ffffff</item> + <item name="android:windowLightStatusBar">true</item> + <item name="android:windowLightNavigationBar">true</item> + <item name="android:navigationBarColor">#ffffff</item> + + </style> + + <style name="bubble_base" parent=""> + <item name="android:textSize">20sp</item> + <item name="android:layout_marginBottom">16dp</item> + </style> + + <style name="bubble" parent="@style/bubble_base"> + <item name="android:layout_marginEnd">56dp</item> + <item name="android:background">@drawable/bubble</item> + <item name="android:layout_gravity">start</item> + </style> + + <style name="bubble_self" parent="@style/bubble_base"> + <item name="android:layout_marginStart">56dp</item> + <item name="android:background">@drawable/bubble_self</item> + <item name="android:layout_gravity">end</item> + </style> + + <color name="primaryColor">#1c3fef</color> + <color name="primaryLightColor">#6f6bff</color> + <color name="primaryDarkColor">#0016bb</color> + <color name="primaryTextColor">#ffffff</color> + + <color name="bubble">#eeeeee</color> + <color name="bubble_self">#D8DCF0</color> + + <dimen name="bubble_corner">16dp</dimen> + <dimen name="bubble_padding">8dp</dimen> + <dimen name="bubble_padding_side">16dp</dimen> + + +</resources>
\ No newline at end of file diff --git a/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/WindowInsetsActivity.java b/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/WindowInsetsActivity.java index b9f5ac0270b2..8e6f1985a7d9 100644 --- a/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/WindowInsetsActivity.java +++ b/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/WindowInsetsActivity.java @@ -18,134 +18,216 @@ package com.google.android.test.windowinsetstests; import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; -import android.animation.ObjectAnimator; -import android.animation.TypeEvaluator; -import android.animation.ValueAnimator; -import android.app.Activity; +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.Property; +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 com.google.android.test.windowinsetstests.R; +import androidx.appcompat.app.AppCompatActivity; +import java.util.ArrayList; import java.util.List; -public class WindowInsetsActivity extends Activity { +public class WindowInsetsActivity extends AppCompatActivity { private View mRoot; - private View mButton; - private static class InsetsProperty extends Property<WindowInsetsAnimationController, Insets> { + final ArrayList<Transition> mTransitions = new ArrayList<>(); - private final View mViewToAnimate; - private final Insets mShowingInsets; + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.window_inset_activity); - public InsetsProperty(View viewToAnimate, Insets showingInsets) { - super(Insets.class, "Insets"); - mViewToAnimate = viewToAnimate; - mShowingInsets = showingInsets; - } + setSupportActionBar(findViewById(R.id.toolbar)); - @Override - public Insets get(WindowInsetsAnimationController object) { - return object.getCurrentInsets(); - } + 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 void set(WindowInsetsAnimationController object, Insets value) { - object.setInsetsAndAlpha(value, 1.0f, 0.5f); - if (mShowingInsets.bottom != 0) { - mViewToAnimate.setTranslationY(mShowingInsets.bottom - value.bottom); - } else if (mShowingInsets.right != 0) { - mViewToAnimate.setTranslationX(mShowingInsets.right - value.right); - } else if (mShowingInsets.left != 0) { - mViewToAnimate.setTranslationX(value.left - mShowingInsets.left); + @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; } - } - }; - float startY; - float endY; - WindowInsetsAnimation imeAnim; + 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)); + } + }); - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.window_inset_activity); - mRoot = findViewById(R.id.root); - mButton = findViewById(R.id.button); - mButton.setOnClickListener(v -> { - if (!v.getRootWindowInsets().isVisible(Type.ime())) { - v.getWindowInsetsController().show(Type.ime()); - } else { - v.getWindowInsetsController().hide(Type.ime()); + 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 WindowInsetsAnimation.Callback( - DISPATCH_MODE_STOP) { + + mRoot.setWindowInsetsAnimationCallback(new Callback(DISPATCH_MODE_STOP) { @Override public void onPrepare(WindowInsetsAnimation animation) { - if ((animation.getTypeMask() & Type.ime()) != 0) { - imeAnim = animation; - } - startY = mButton.getTop(); + mTransitions.forEach(it -> it.onPrepare(animation)); } @Override public WindowInsets onProgress(WindowInsets insets, - List<WindowInsetsAnimation> runningAnimations) { - mButton.setY(startY + (endY - startY) * imeAnim.getInterpolatedFraction()); + @NonNull List<WindowInsetsAnimation> runningAnimations) { + mTransitions.forEach(it -> it.onProgress(insets)); return insets; } @Override public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds) { - endY = mButton.getTop(); + mTransitions.forEach(Transition::onStart); return bounds; } @Override public void onEnd(WindowInsetsAnimation animation) { - imeAnim = null; + mTransitions.forEach(it -> it.onFinish(animation)); } }); } @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); + public void onResume() { + super.onResume(); + // TODO: move this to onCreate once setDecorFitsSystemWindows can be safely called there. + getWindow().getDecorView().post(() -> getWindow().setDecorFitsSystemWindows(false)); + } - TypeEvaluator<Insets> evaluator = (fraction, startValue, endValue) -> Insets.of( - (int)(startValue.left + fraction * (endValue.left - startValue.left)), - (int)(startValue.top + fraction * (endValue.top - startValue.top)), - (int)(startValue.right + fraction * (endValue.right - startValue.right)), - (int)(startValue.bottom + fraction * (endValue.bottom - startValue.bottom))); + static class Transition { + private int mEndBottom; + private int mStartBottom; + private final View mView; + private WindowInsetsAnimation mInsetsAnimation; - WindowInsetsAnimationControlListener listener = new WindowInsetsAnimationControlListener() { - @Override - public void onReady(WindowInsetsAnimationController controller, int types) { - ObjectAnimator animator = ObjectAnimator.ofObject(controller, - new InsetsProperty(findViewById(R.id.button), - controller.getShownStateInsets()), - evaluator, controller.getShownStateInsets(), - controller.getHiddenStateInsets()); - animator.setRepeatCount(ValueAnimator.INFINITE); - animator.setRepeatMode(ValueAnimator.REVERSE); - animator.start(); + Transition(View root) { + mView = root; + } + + void onPrepare(WindowInsetsAnimation animation) { + if ((animation.getTypeMask() & Type.ime()) != 0) { + mInsetsAnimation = animation; } + mStartBottom = mView.getBottom(); + } - @Override - public void onCancelled() { + 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); + } } } |