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

Commit d47c7354 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Use settingslib ActionBarShadowController"

parents e798c078 1107d310
Loading
Loading
Loading
Loading
+0 −134
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.settings.widget;

import android.app.ActionBar;
import android.app.Activity;
import android.view.View;

import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.RecyclerView;

import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;

/**
 * A controller that adds shadow to actionbar when content view scrolls.
 * <p/>
 * It also works on custom views acting as an actionbar.
 */
public class ActionBarShadowController implements LifecycleObserver, OnStart, OnStop {

    @VisibleForTesting
    static final float ELEVATION_HIGH = 8;
    @VisibleForTesting
    static final float ELEVATION_LOW = 0;

    @VisibleForTesting
    ScrollChangeWatcher mScrollChangeWatcher;
    private RecyclerView mRecyclerView;
    private boolean isScrollWatcherAttached;

    public static ActionBarShadowController attachToRecyclerView(Activity activity,
            Lifecycle lifecycle, RecyclerView recyclerView) {
        return new ActionBarShadowController(activity, lifecycle, recyclerView);
    }

    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 ScrollChangeWatcher(activity);
        mRecyclerView = recyclerView;
        attachScrollWatcher();
        lifecycle.addObserver(this);
    }

    private ActionBarShadowController(View anchorView, Lifecycle lifecycle,
            RecyclerView recyclerView) {
        mScrollChangeWatcher = new ScrollChangeWatcher(anchorView);
        mRecyclerView = recyclerView;
        attachScrollWatcher();
        lifecycle.addObserver(this);
    }

    @Override
    public void onStop() {
        detachScrollWatcher();
    }

    private void detachScrollWatcher() {
        mRecyclerView.removeOnScrollListener(mScrollChangeWatcher);
        isScrollWatcherAttached = false;
    }

    @Override
    public void onStart() {
        attachScrollWatcher();
    }

    private void attachScrollWatcher() {
        if (!isScrollWatcherAttached) {
            isScrollWatcherAttached = true;
            mRecyclerView.addOnScrollListener(mScrollChangeWatcher);
            mScrollChangeWatcher.updateDropShadow(mRecyclerView);
        }
    }

    /**
     * Update the drop shadow as the scrollable entity is scrolled.
     */
    final class ScrollChangeWatcher extends RecyclerView.OnScrollListener {

        private final Activity mActivity;
        private final View mAnchorView;

        public ScrollChangeWatcher(Activity activity) {
            mActivity = activity;
            mAnchorView = null;
        }

        public 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
@@ -49,6 +49,7 @@ import com.android.settings.applications.appinfo.AppInfoDashboardFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.widget.ActionBarShadowController;
import com.android.settingslib.widget.LayoutPreference;

import java.lang.annotation.Retention;
+0 −116
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.settings.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
    }
}