diff options
author | 2019-01-29 15:38:13 -0800 | |
---|---|---|
committer | 2019-01-29 15:51:31 -0800 | |
commit | 391da986af51bd4be1dd4075ae2e937fc6f7fa38 (patch) | |
tree | bc12c9b95973fae98c9ad81bc2087810421ea859 | |
parent | 87969723fc13cd69784fffb7ed3e8f8f6eed1a14 (diff) |
Move ActionBarShadowController to settingslib.
And remove dependency to settingslib.core.Lifecycle. This class use
androidx.Lifecycle instead.
Bug: 123311100
Test: robotests
Change-Id: I80bbbf4bc2759e574d8dabf1799b3bded216f2f3
5 files changed, 287 insertions, 0 deletions
diff --git a/packages/SettingsLib/ActionBarShadow/Android.bp b/packages/SettingsLib/ActionBarShadow/Android.bp new file mode 100644 index 000000000000..d2848564abea --- /dev/null +++ b/packages/SettingsLib/ActionBarShadow/Android.bp @@ -0,0 +1,14 @@ +android_library { + name: "SettingsLibActionBarShadow", + + srcs: ["src/**/*.java"], + + static_libs: [ + "androidx.annotation_annotation", + "androidx.lifecycle_lifecycle-runtime", + "androidx.recyclerview_recyclerview", + ], + + sdk_version: "system_current", + min_sdk_version: "21", +} diff --git a/packages/SettingsLib/ActionBarShadow/AndroidManifest.xml b/packages/SettingsLib/ActionBarShadow/AndroidManifest.xml new file mode 100644 index 000000000000..4b9f1ab8d6cc --- /dev/null +++ b/packages/SettingsLib/ActionBarShadow/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.settingslib.widget"> + + <uses-sdk android:minSdkVersion="21" /> + +</manifest> diff --git a/packages/SettingsLib/ActionBarShadow/src/com/android/settingslib/widget/ActionBarShadowController.java b/packages/SettingsLib/ActionBarShadow/src/com/android/settingslib/widget/ActionBarShadowController.java new file mode 100644 index 000000000000..68ba5fbd120f --- /dev/null +++ b/packages/SettingsLib/ActionBarShadow/src/com/android/settingslib/widget/ActionBarShadowController.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2019 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.android.settingslib.widget; + +import static androidx.lifecycle.Lifecycle.Event.ON_START; +import static androidx.lifecycle.Lifecycle.Event.ON_STOP; + +import android.app.ActionBar; +import android.app.Activity; +import android.view.View; + +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.recyclerview.widget.RecyclerView; + +/** + * UI controller that adds a shadow appear/disappear animation to action bar scroll. + */ +public class ActionBarShadowController implements LifecycleObserver { + + @VisibleForTesting + static final float ELEVATION_HIGH = 8; + @VisibleForTesting + static final float ELEVATION_LOW = 0; + + @VisibleForTesting + ScrollChangeWatcher mScrollChangeWatcher; + private RecyclerView mRecyclerView; + private boolean mIsScrollWatcherAttached; + + /** + * Wire up the animation to to an {@link Activity}. Shadow will be applied to activity's + * action bar. + */ + public static ActionBarShadowController attachToRecyclerView( + Activity activity, Lifecycle lifecycle, RecyclerView recyclerView) { + return new ActionBarShadowController(activity, lifecycle, recyclerView); + } + + /** + * Wire up the animation to to a {@link View}. Shadow will be applied to the view. + */ + public static ActionBarShadowController attachToRecyclerView( + View anchorView, Lifecycle lifecycle, RecyclerView recyclerView) { + return new ActionBarShadowController(anchorView, lifecycle, recyclerView); + } + + private ActionBarShadowController(Activity activity, Lifecycle lifecycle, + RecyclerView recyclerView) { + mScrollChangeWatcher = + new ActionBarShadowController.ScrollChangeWatcher(activity); + mRecyclerView = recyclerView; + attachScrollWatcher(); + lifecycle.addObserver(this); + } + + private ActionBarShadowController(View anchorView, Lifecycle lifecycle, + RecyclerView recyclerView) { + mScrollChangeWatcher = + new ActionBarShadowController.ScrollChangeWatcher(anchorView); + mRecyclerView = recyclerView; + attachScrollWatcher(); + lifecycle.addObserver(this); + } + + @OnLifecycleEvent(ON_START) + private void attachScrollWatcher() { + if (!mIsScrollWatcherAttached) { + mIsScrollWatcherAttached = true; + mRecyclerView.addOnScrollListener(mScrollChangeWatcher); + mScrollChangeWatcher.updateDropShadow(mRecyclerView); + } + } + + @OnLifecycleEvent(ON_STOP) + private void detachScrollWatcher() { + mRecyclerView.removeOnScrollListener(mScrollChangeWatcher); + mIsScrollWatcherAttached = false; + } + + /** + * Update the drop shadow as the scrollable entity is scrolled. + */ + final class ScrollChangeWatcher extends RecyclerView.OnScrollListener { + + private final Activity mActivity; + private final View mAnchorView; + + ScrollChangeWatcher(Activity activity) { + mActivity = activity; + mAnchorView = null; + } + + ScrollChangeWatcher(View anchorView) { + mAnchorView = anchorView; + mActivity = null; + } + + // RecyclerView scrolled. + @Override + public void onScrolled(RecyclerView view, int dx, int dy) { + updateDropShadow(view); + } + + public void updateDropShadow(View view) { + final boolean shouldShowShadow = view.canScrollVertically(-1); + if (mAnchorView != null) { + mAnchorView.setElevation(shouldShowShadow ? ELEVATION_HIGH : ELEVATION_LOW); + } else if (mActivity != null) { // activity can become null when running monkey + final ActionBar actionBar = mActivity.getActionBar(); + if (actionBar != null) { + actionBar.setElevation(shouldShowShadow ? ELEVATION_HIGH : ELEVATION_LOW); + } + } + } + } +} diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index 2321790fa960..5a81f8bc6566 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -13,6 +13,7 @@ android_library { "SettingsLibHelpUtils", "SettingsLibRestrictedLockUtils", + "SettingsLibActionBarShadow", "SettingsLibAppPreference", "SettingsLibSearchWidget", "SettingsLibSettingsSpinner", diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/ActionBarShadowControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/ActionBarShadowControllerTest.java new file mode 100644 index 000000000000..a25b51298d3e --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/ActionBarShadowControllerTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2019 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.android.settingslib.widget; + +import static androidx.lifecycle.Lifecycle.Event.ON_START; +import static androidx.lifecycle.Lifecycle.Event.ON_STOP; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActionBar; +import android.app.Activity; +import android.view.View; + +import androidx.lifecycle.LifecycleOwner; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class ActionBarShadowControllerTest { + + @Mock + private RecyclerView mRecyclerView; + @Mock + private Activity mActivity; + @Mock + private ActionBar mActionBar; + private Lifecycle mLifecycle; + private LifecycleOwner mLifecycleOwner; + private View mView; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mActivity.getActionBar()).thenReturn(mActionBar); + mView = new View(RuntimeEnvironment.application); + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); + } + + @Test + public void attachToRecyclerView_shouldAddScrollWatcherAndUpdateActionBar() { + when(mRecyclerView.canScrollVertically(-1)).thenReturn(false); + + ActionBarShadowController.attachToRecyclerView(mActivity, mLifecycle, mRecyclerView); + + verify(mActionBar).setElevation(ActionBarShadowController.ELEVATION_LOW); + } + + @Test + public void attachToRecyclerView_customViewAsActionBar_shouldUpdateElevationOnScroll() { + // Setup + mView.setElevation(50); + when(mRecyclerView.canScrollVertically(-1)).thenReturn(false); + final ActionBarShadowController controller = + ActionBarShadowController.attachToRecyclerView(mView, mLifecycle, mRecyclerView); + assertThat(mView.getElevation()).isEqualTo(ActionBarShadowController.ELEVATION_LOW); + + // Scroll + when(mRecyclerView.canScrollVertically(-1)).thenReturn(true); + controller.mScrollChangeWatcher.onScrolled(mRecyclerView, 10 /* dx */, 10 /* dy */); + assertThat(mView.getElevation()).isEqualTo(ActionBarShadowController.ELEVATION_HIGH); + } + + @Test + public void attachToRecyclerView_lifecycleChange_shouldAttachDetach() { + ActionBarShadowController.attachToRecyclerView(mActivity, mLifecycle, mRecyclerView); + + verify(mRecyclerView).addOnScrollListener(any()); + + mLifecycle.handleLifecycleEvent(ON_START); + mLifecycle.handleLifecycleEvent(ON_STOP); + verify(mRecyclerView).removeOnScrollListener(any()); + + mLifecycle.handleLifecycleEvent(ON_START); + verify(mRecyclerView, times(2)).addOnScrollListener(any()); + } + + @Test + public void onScrolled_nullAnchorViewAndActivity_shouldNotCrash() { + final Activity activity = null; + final ActionBarShadowController controller = + ActionBarShadowController.attachToRecyclerView(activity, mLifecycle, mRecyclerView); + + // Scroll + controller.mScrollChangeWatcher.onScrolled(mRecyclerView, 10 /* dx */, 10 /* dy */); + // no crash + } +} |