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

Commit 9b458a00 authored by Sunny Goyal's avatar Sunny Goyal
Browse files

Generalizing the PredicitonScroll view so that in can be used in all-apps

Bug: 234008165
Test: Verified no-functionality-change on device
Change-Id: Ie17d58148b9bdcb08847beb24114b0494437b30e
parent 420ab0a4
Loading
Loading
Loading
Loading
+5 −4
Original line number Diff line number Diff line
@@ -41,7 +41,7 @@
    </com.android.launcher3.widget.picker.WidgetPagedView>

    <!-- SearchAndRecommendationsView contains the tab layout as well -->
    <com.android.launcher3.widget.picker.SearchAndRecommendationsView
    <com.android.launcher3.views.StickyHeaderLayout
        android:id="@+id/search_and_recommendations_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
@@ -68,7 +68,7 @@
            android:background="?android:attr/colorBackground"
            android:paddingBottom="8dp"
            android:paddingHorizontal="@dimen/widget_list_horizontal_margin"
            android:clipToPadding="false">
            launcher:layout_sticky="true">
            <include layout="@layout/widgets_search_bar" />
        </FrameLayout>

@@ -92,7 +92,8 @@
            android:paddingLeft="@dimen/widget_tabs_horizontal_padding"
            android:paddingRight="@dimen/widget_tabs_horizontal_padding"
            android:background="?android:attr/colorBackground"
            style="@style/TextHeadline">
            style="@style/TextHeadline"
            launcher:layout_sticky="true">

            <Button
                android:id="@+id/tab_personal"
@@ -121,5 +122,5 @@
                style="?android:attr/borderlessButtonStyle" />
        </com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip>

    </com.android.launcher3.widget.picker.SearchAndRecommendationsView>
    </com.android.launcher3.views.StickyHeaderLayout>
</merge>
 No newline at end of file
+6 −4
Original line number Diff line number Diff line
@@ -13,7 +13,8 @@
     See the License for the specific language governing permissions and
     limitations under the License.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:launcher="http://schemas.android.com/apk/res-auto" >
    <com.android.launcher3.widget.picker.WidgetsRecyclerView
        android:id="@+id/primary_widgets_list_view"
        android:layout_below="@id/collapse_handle"
@@ -23,7 +24,7 @@
        android:clipToPadding="false" />

    <!-- SearchAndRecommendationsView without the tab layout as well -->
    <com.android.launcher3.widget.picker.SearchAndRecommendationsView
    <com.android.launcher3.views.StickyHeaderLayout
        android:id="@+id/search_and_recommendations_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
@@ -50,7 +51,8 @@
            android:background="?android:attr/colorBackground"
            android:paddingHorizontal="@dimen/widget_list_horizontal_margin"
            android:paddingBottom="8dp"
            android:clipToPadding="false">
            android:clipToPadding="false"
            launcher:layout_sticky="true" >
            <include layout="@layout/widgets_search_bar" />
        </FrameLayout>

@@ -63,6 +65,6 @@
            android:paddingVertical="@dimen/recommended_widgets_table_vertical_padding"
            android:paddingHorizontal="@dimen/widget_list_horizontal_margin"
            android:visibility="gone" />
    </com.android.launcher3.widget.picker.SearchAndRecommendationsView>
    </com.android.launcher3.views.StickyHeaderLayout>

</merge>
 No newline at end of file
+4 −0
Original line number Diff line number Diff line
@@ -136,6 +136,10 @@
        <attr name="layout_ignoreInsets" format="boolean" />
    </declare-styleable>

    <declare-styleable name="StickyScroller_Layout">
        <attr name="layout_sticky" format="boolean" />
    </declare-styleable>

    <declare-styleable name="GridDisplayOption">
        <attr name="name" format="string" />

+327 −0
Original line number Diff line number Diff line
@@ -13,58 +13,55 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.launcher3.widget.picker;
package com.android.launcher3.views;

import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.makeMeasureSpec;

import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;

import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.LinearLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;

import com.android.launcher3.R;
import com.android.launcher3.widget.picker.WidgetsSpaceViewHolderBinder.EmptySpaceView;
import com.android.launcher3.widget.picker.search.WidgetsSearchBar;

/**
 * A controller which measures & updates {@link WidgetsFullSheet}'s views padding, margin and
 * vertical displacement upon scrolling.
 * A {@link LinearLayout} container which allows scrolling parts of its content based on the
 * scroll of a different view. Views which are marked as sticky are not scrolled, giving the
 * illusion of a sticky header.
 */
final class SearchAndRecommendationsScrollController implements
public class StickyHeaderLayout extends LinearLayout implements
        RecyclerView.OnChildAttachStateChangeListener {

    private static final FloatProperty<SearchAndRecommendationsScrollController> SCROLL_OFFSET =
            new FloatProperty<SearchAndRecommendationsScrollController>("scrollAnimOffset") {
    private static final FloatProperty<StickyHeaderLayout> SCROLL_OFFSET =
            new FloatProperty<StickyHeaderLayout>("scrollAnimOffset") {
                @Override
        public void setValue(SearchAndRecommendationsScrollController controller, float offset) {
            controller.mScrollOffset = offset;
            controller.updateHeaderScroll();
                public void setValue(StickyHeaderLayout view, float offset) {
                    view.mScrollOffset = offset;
                    view.updateHeaderScroll();
                }

                @Override
        public Float get(SearchAndRecommendationsScrollController controller) {
            return controller.mScrollOffset;
                public Float get(StickyHeaderLayout view) {
                    return view.mScrollOffset;
                }
            };

    private static final MotionEventProxyMethod INTERCEPT_PROXY = ViewGroup::onInterceptTouchEvent;
    private static final MotionEventProxyMethod TOUCH_PROXY = ViewGroup::onTouchEvent;

    final SearchAndRecommendationsView mContainer;
    final View mSearchBarContainer;
    final WidgetsSearchBar mSearchBar;
    final TextView mHeaderTitle;
    final WidgetsRecommendationTableLayout mRecommendedWidgetsTable;
    @Nullable final View mTabBar;

    private WidgetsRecyclerView mCurrentRecyclerView;
    private RecyclerView mCurrentRecyclerView;
    private EmptySpaceView mCurrentEmptySpaceView;

    private float mLastScroll = 0;
@@ -72,22 +69,29 @@ final class SearchAndRecommendationsScrollController implements
    private Animator mOffsetAnimator;

    private boolean mShouldForwardToRecyclerView = false;

    private int mHeaderHeight;

    SearchAndRecommendationsScrollController(
            SearchAndRecommendationsView searchAndRecommendationContainer) {
        mContainer = searchAndRecommendationContainer;
        mSearchBarContainer = mContainer.findViewById(R.id.search_bar_container);
        mSearchBar = mContainer.findViewById(R.id.widgets_search_bar);
        mHeaderTitle = mContainer.findViewById(R.id.title);
        mRecommendedWidgetsTable = mContainer.findViewById(R.id.recommended_widget_table);
        mTabBar = mContainer.findViewById(R.id.tabs);
    public StickyHeaderLayout(Context context) {
        this(context, /* attrs= */ null);
    }

    public StickyHeaderLayout(Context context, AttributeSet attrs) {
        this(context, attrs, /* defStyleAttr= */ 0);
    }

    public StickyHeaderLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, /* defStyleRes= */ 0);
    }

        mContainer.setSearchAndRecommendationScrollController(this);
    public StickyHeaderLayout(Context context, AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    public void setCurrentRecyclerView(WidgetsRecyclerView currentRecyclerView) {
    /**
     * Sets the recycler view, this sticky header should track
     */
    public void setCurrentRecyclerView(RecyclerView currentRecyclerView) {
        boolean animateReset = mCurrentRecyclerView != null;
        if (mCurrentRecyclerView != null) {
            mCurrentRecyclerView.removeOnChildAttachStateChangeListener(this);
@@ -104,16 +108,11 @@ final class SearchAndRecommendationsScrollController implements

    private void updateHeaderScroll() {
        mLastScroll = getCurrentScroll();
        mHeaderTitle.setTranslationY(mLastScroll);
        mRecommendedWidgetsTable.setTranslationY(mLastScroll);

        float searchYDisplacement = Math.max(mLastScroll, -mSearchBarContainer.getTop());
        mSearchBarContainer.setTranslationY(searchYDisplacement);

        if (mTabBar != null) {
            float tabsDisplacement = Math.max(mLastScroll, -mTabBar.getTop()
                    + mSearchBarContainer.getHeight());
            mTabBar.setTranslationY(tabsDisplacement);
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            MyLayoutParams lp = (MyLayoutParams) child.getLayoutParams();
            child.setTranslationY(Math.max(mLastScroll, lp.scrollLimit));
        }
    }

@@ -121,25 +120,14 @@ final class SearchAndRecommendationsScrollController implements
        return mScrollOffset + (mCurrentEmptySpaceView == null ? 0 : mCurrentEmptySpaceView.getY());
    }

    /**
     * Updates the scrollable header height
     *
     * @return {@code true} if the header height or dependent property changed.
     */
    public boolean updateHeaderHeight() {
        boolean hasSizeUpdated = false;

        int headerHeight = mContainer.getMeasuredHeight();
        if (headerHeight != mHeaderHeight) {
            mHeaderHeight = headerHeight;
            hasSizeUpdated = true;
        }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if (mCurrentEmptySpaceView != null
                && mCurrentEmptySpaceView.setFixedHeight(mHeaderHeight)) {
            hasSizeUpdated = true;
        mHeaderHeight = getMeasuredHeight();
        if (mCurrentEmptySpaceView != null) {
            mCurrentEmptySpaceView.setFixedHeight(mHeaderHeight);
        }
        return hasSizeUpdated;
    }

    /** Resets any previous view translation. */
@@ -160,23 +148,21 @@ final class SearchAndRecommendationsScrollController implements
        }
    }

    /**
     * Returns {@code true} if a touch event should be intercepted by this controller.
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        return (mShouldForwardToRecyclerView = proxyMotionEvent(event, INTERCEPT_PROXY));
        return (mShouldForwardToRecyclerView = proxyMotionEvent(event, INTERCEPT_PROXY))
                || super.onInterceptTouchEvent(event);
    }

    /**
     * Returns {@code true} if this controller has intercepted and consumed a touch event.
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mShouldForwardToRecyclerView && proxyMotionEvent(event, TOUCH_PROXY);
        return mShouldForwardToRecyclerView && proxyMotionEvent(event, TOUCH_PROXY)
                || super.onTouchEvent(event);
    }

    private boolean proxyMotionEvent(MotionEvent event, MotionEventProxyMethod method) {
        float dx = mCurrentRecyclerView.getLeft() - mContainer.getLeft();
        float dy = mCurrentRecyclerView.getTop() - mContainer.getTop();
        float dx = mCurrentRecyclerView.getLeft() - getLeft();
        float dy = mCurrentRecyclerView.getTop() - getTop();
        event.offsetLocation(dx, dy);
        try {
            return method.proxyEvent(mCurrentRecyclerView, event);
@@ -216,8 +202,126 @@ final class SearchAndRecommendationsScrollController implements
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);

        // Update various stick parameters
        int count = getChildCount();
        int stickyHeaderHeight = 0;
        for (int i = 0; i < count; i++) {
            View v = getChildAt(i);
            MyLayoutParams lp = (MyLayoutParams) v.getLayoutParams();
            if (lp.sticky) {
                lp.scrollLimit = -v.getTop() + stickyHeaderHeight;
                stickyHeaderHeight += v.getHeight();
            } else {
                lp.scrollLimit = Integer.MIN_VALUE;
            }
        }
        updateHeaderScroll();
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MyLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
        return new MyLayoutParams(lp.width, lp.height);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MyLayoutParams(getContext(), attrs);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof MyLayoutParams;
    }

    private static class MyLayoutParams extends LayoutParams {

        public final boolean sticky;
        public int scrollLimit;

        MyLayoutParams(int width, int height) {
            super(width, height);
            sticky = false;
        }

        MyLayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.StickyScroller_Layout);
            sticky = a.getBoolean(R.styleable.StickyScroller_Layout_layout_sticky, false);
            a.recycle();
        }
    }

    private interface MotionEventProxyMethod {

        boolean proxyEvent(ViewGroup view, MotionEvent event);
    }

    /**
     * Empty view which allows listening for 'Y' changes
     */
    public static class EmptySpaceView extends View {

        private Runnable mOnYChangeCallback;
        private int mHeight = 0;

        public EmptySpaceView(Context context) {
            super(context);
            animate().setUpdateListener(v -> notifyYChanged());
        }

        /**
         * Sets the height for the empty view
         * @return true if the height changed, false otherwise
         */
        public boolean setFixedHeight(int height) {
            if (mHeight != height) {
                mHeight = height;
                requestLayout();
                return true;
            }
            return false;
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, makeMeasureSpec(mHeight, EXACTLY));
        }

        public void setOnYChangeCallback(Runnable callback) {
            mOnYChangeCallback = callback;
        }

        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            super.onLayout(changed, left, top, right, bottom);
            notifyYChanged();
        }

        @Override
        public void offsetTopAndBottom(int offset) {
            super.offsetTopAndBottom(offset);
            notifyYChanged();
        }

        @Override
        public void setTranslationY(float translationY) {
            super.setTranslationY(translationY);
            notifyYChanged();
        }

        private void notifyYChanged() {
            if (mOnYChangeCallback != null) {
                mOnYChangeCallback.run();
            }
        }
    }
}
+0 −62
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.launcher3.widget.picker;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.LinearLayout;

/**
 * A {@link LinearLayout} container for holding search and widgets recommendation.
 *
 * <p>This class intercepts touch events and dispatch them to the right view.
 */
public class SearchAndRecommendationsView extends LinearLayout {
    private SearchAndRecommendationsScrollController mController;

    public SearchAndRecommendationsView(Context context) {
        this(context, /* attrs= */ null);
    }

    public SearchAndRecommendationsView(Context context, AttributeSet attrs) {
        this(context, attrs, /* defStyleAttr= */ 0);
    }

    public SearchAndRecommendationsView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, /* defStyleRes= */ 0);
    }

    public SearchAndRecommendationsView(Context context, AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    public void setSearchAndRecommendationScrollController(
            SearchAndRecommendationsScrollController controller) {
        mController = controller;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        return mController.onInterceptTouchEvent(event) || super.onInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mController.onTouchEvent(event) || super.onTouchEvent(event);
    }
}
Loading