Merge "WindowInsetsTests: More compelling IME transition sample"

This commit is contained in:
Jorim Jaggi
2020-02-18 16:20:58 +00:00
committed by Android (Google) Code Review
11 changed files with 433 additions and 85 deletions

View File

@@ -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",
],
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -16,5 +16,5 @@
-->
<resources>
<string name="activity_title">Window Insets Tests</string>
<string name="activity_title">New Insets Chat</string>
</resources>

View File

@@ -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>

View File

@@ -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> {
private final View mViewToAnimate;
private final Insets mShowingInsets;
public InsetsProperty(View viewToAnimate, Insets showingInsets) {
super(Insets.class, "Insets");
mViewToAnimate = viewToAnimate;
mShowingInsets = showingInsets;
}
@Override
public Insets get(WindowInsetsAnimationController object) {
return object.getCurrentInsets();
}
@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);
}
}
};
float startY;
float endY;
WindowInsetsAnimation imeAnim;
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);
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.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.setWindowInsetsAnimationCallback(new WindowInsetsAnimation.Callback(
DISPATCH_MODE_STOP) {
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) {
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);
}
}
}