Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 391da986 authored by Fan Zhang's avatar Fan Zhang
Browse files

Move ActionBarShadowController to settingslib.

And remove dependency to settingslib.core.Lifecycle. This class use
androidx.Lifecycle instead.

Bug: 123311100
Test: robotests
Change-Id: I80bbbf4bc2759e574d8dabf1799b3bded216f2f3
parent 87969723
Loading
Loading
Loading
Loading
+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",
}
+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>
+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);
                }
            }
        }
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -13,6 +13,7 @@ android_library {

        "SettingsLibHelpUtils",
        "SettingsLibRestrictedLockUtils",
        "SettingsLibActionBarShadow",
        "SettingsLibAppPreference",
        "SettingsLibSearchWidget",
        "SettingsLibSettingsSpinner",
+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
    }
}