Loading packages/SettingsLib/ActionBarShadow/Android.bp 0 → 100644 +14 −0 Original line number Diff line number Diff line 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", } packages/SettingsLib/ActionBarShadow/AndroidManifest.xml 0 → 100644 +23 −0 Original line number Diff line number Diff line <?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> packages/SettingsLib/ActionBarShadow/src/com/android/settingslib/widget/ActionBarShadowController.java 0 → 100644 +133 −0 Original line number Diff line number Diff line /* * 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); } } } } } packages/SettingsLib/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -13,6 +13,7 @@ android_library { "SettingsLibHelpUtils", "SettingsLibRestrictedLockUtils", "SettingsLibActionBarShadow", "SettingsLibAppPreference", "SettingsLibSearchWidget", "SettingsLibSettingsSpinner", Loading packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/ActionBarShadowControllerTest.java 0 → 100644 +116 −0 Original line number Diff line number Diff line /* * 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 } } Loading
packages/SettingsLib/ActionBarShadow/Android.bp 0 → 100644 +14 −0 Original line number Diff line number Diff line 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", }
packages/SettingsLib/ActionBarShadow/AndroidManifest.xml 0 → 100644 +23 −0 Original line number Diff line number Diff line <?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>
packages/SettingsLib/ActionBarShadow/src/com/android/settingslib/widget/ActionBarShadowController.java 0 → 100644 +133 −0 Original line number Diff line number Diff line /* * 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); } } } } }
packages/SettingsLib/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -13,6 +13,7 @@ android_library { "SettingsLibHelpUtils", "SettingsLibRestrictedLockUtils", "SettingsLibActionBarShadow", "SettingsLibAppPreference", "SettingsLibSearchWidget", "SettingsLibSettingsSpinner", Loading
packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/ActionBarShadowControllerTest.java 0 → 100644 +116 −0 Original line number Diff line number Diff line /* * 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 } }