Merge "Generalizing the PredicitonScroll view so that in can be used in all-apps" into tm-qpr-dev am: eb966492f7
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Launcher3/+/19089102 Change-Id: I2cfd2523cd4b9ef0f695fbd41a6293657e5b8a50 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
/*
|
||||
* 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.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.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.android.launcher3.R;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public class StickyHeaderLayout extends LinearLayout implements
|
||||
RecyclerView.OnChildAttachStateChangeListener {
|
||||
|
||||
private static final FloatProperty<StickyHeaderLayout> SCROLL_OFFSET =
|
||||
new FloatProperty<StickyHeaderLayout>("scrollAnimOffset") {
|
||||
@Override
|
||||
public void setValue(StickyHeaderLayout view, float offset) {
|
||||
view.mScrollOffset = offset;
|
||||
view.updateHeaderScroll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Float get(StickyHeaderLayout view) {
|
||||
return view.mScrollOffset;
|
||||
}
|
||||
};
|
||||
|
||||
private static final MotionEventProxyMethod INTERCEPT_PROXY = ViewGroup::onInterceptTouchEvent;
|
||||
private static final MotionEventProxyMethod TOUCH_PROXY = ViewGroup::onTouchEvent;
|
||||
|
||||
private RecyclerView mCurrentRecyclerView;
|
||||
private EmptySpaceView mCurrentEmptySpaceView;
|
||||
|
||||
private float mLastScroll = 0;
|
||||
private float mScrollOffset = 0;
|
||||
private Animator mOffsetAnimator;
|
||||
|
||||
private boolean mShouldForwardToRecyclerView = false;
|
||||
private int mHeaderHeight;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public StickyHeaderLayout(Context context, AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the recycler view, this sticky header should track
|
||||
*/
|
||||
public void setCurrentRecyclerView(RecyclerView currentRecyclerView) {
|
||||
boolean animateReset = mCurrentRecyclerView != null;
|
||||
if (mCurrentRecyclerView != null) {
|
||||
mCurrentRecyclerView.removeOnChildAttachStateChangeListener(this);
|
||||
}
|
||||
mCurrentRecyclerView = currentRecyclerView;
|
||||
mCurrentRecyclerView.addOnChildAttachStateChangeListener(this);
|
||||
findCurrentEmptyView();
|
||||
reset(animateReset);
|
||||
}
|
||||
|
||||
public int getHeaderHeight() {
|
||||
return mHeaderHeight;
|
||||
}
|
||||
|
||||
private void updateHeaderScroll() {
|
||||
mLastScroll = getCurrentScroll();
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
private float getCurrentScroll() {
|
||||
return mScrollOffset + (mCurrentEmptySpaceView == null ? 0 : mCurrentEmptySpaceView.getY());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
mHeaderHeight = getMeasuredHeight();
|
||||
if (mCurrentEmptySpaceView != null) {
|
||||
mCurrentEmptySpaceView.setFixedHeight(mHeaderHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/** Resets any previous view translation. */
|
||||
public void reset(boolean animate) {
|
||||
if (mOffsetAnimator != null) {
|
||||
mOffsetAnimator.cancel();
|
||||
mOffsetAnimator = null;
|
||||
}
|
||||
|
||||
mScrollOffset = 0;
|
||||
if (!animate) {
|
||||
updateHeaderScroll();
|
||||
} else {
|
||||
float startValue = mLastScroll - getCurrentScroll();
|
||||
mOffsetAnimator = ObjectAnimator.ofFloat(this, SCROLL_OFFSET, startValue, 0);
|
||||
mOffsetAnimator.addListener(forEndCallback(() -> mOffsetAnimator = null));
|
||||
mOffsetAnimator.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent event) {
|
||||
return (mShouldForwardToRecyclerView = proxyMotionEvent(event, INTERCEPT_PROXY))
|
||||
|| super.onInterceptTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
return mShouldForwardToRecyclerView && proxyMotionEvent(event, TOUCH_PROXY)
|
||||
|| super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
private boolean proxyMotionEvent(MotionEvent event, MotionEventProxyMethod method) {
|
||||
float dx = mCurrentRecyclerView.getLeft() - getLeft();
|
||||
float dy = mCurrentRecyclerView.getTop() - getTop();
|
||||
event.offsetLocation(dx, dy);
|
||||
try {
|
||||
return method.proxyEvent(mCurrentRecyclerView, event);
|
||||
} finally {
|
||||
event.offsetLocation(-dx, -dy);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildViewAttachedToWindow(@NonNull View view) {
|
||||
if (view instanceof EmptySpaceView) {
|
||||
findCurrentEmptyView();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildViewDetachedFromWindow(@NonNull View view) {
|
||||
if (view == mCurrentEmptySpaceView) {
|
||||
findCurrentEmptyView();
|
||||
}
|
||||
}
|
||||
|
||||
private void findCurrentEmptyView() {
|
||||
if (mCurrentEmptySpaceView != null) {
|
||||
mCurrentEmptySpaceView.setOnYChangeCallback(null);
|
||||
mCurrentEmptySpaceView = null;
|
||||
}
|
||||
int childCount = mCurrentRecyclerView.getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
View view = mCurrentRecyclerView.getChildAt(i);
|
||||
if (view instanceof EmptySpaceView) {
|
||||
mCurrentEmptySpaceView = (EmptySpaceView) view;
|
||||
mCurrentEmptySpaceView.setFixedHeight(getHeaderHeight());
|
||||
mCurrentEmptySpaceView.setOnYChangeCallback(this::updateHeaderScroll);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-223
@@ -1,223 +0,0 @@
|
||||
/*
|
||||
* 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 static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.util.FloatProperty;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
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.
|
||||
*/
|
||||
final class SearchAndRecommendationsScrollController implements
|
||||
RecyclerView.OnChildAttachStateChangeListener {
|
||||
|
||||
private static final FloatProperty<SearchAndRecommendationsScrollController> SCROLL_OFFSET =
|
||||
new FloatProperty<SearchAndRecommendationsScrollController>("scrollAnimOffset") {
|
||||
@Override
|
||||
public void setValue(SearchAndRecommendationsScrollController controller, float offset) {
|
||||
controller.mScrollOffset = offset;
|
||||
controller.updateHeaderScroll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Float get(SearchAndRecommendationsScrollController controller) {
|
||||
return controller.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 EmptySpaceView mCurrentEmptySpaceView;
|
||||
|
||||
private float mLastScroll = 0;
|
||||
private float mScrollOffset = 0;
|
||||
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);
|
||||
|
||||
mContainer.setSearchAndRecommendationScrollController(this);
|
||||
}
|
||||
|
||||
public void setCurrentRecyclerView(WidgetsRecyclerView currentRecyclerView) {
|
||||
boolean animateReset = mCurrentRecyclerView != null;
|
||||
if (mCurrentRecyclerView != null) {
|
||||
mCurrentRecyclerView.removeOnChildAttachStateChangeListener(this);
|
||||
}
|
||||
mCurrentRecyclerView = currentRecyclerView;
|
||||
mCurrentRecyclerView.addOnChildAttachStateChangeListener(this);
|
||||
findCurrentEmptyView();
|
||||
reset(animateReset);
|
||||
}
|
||||
|
||||
public int getHeaderHeight() {
|
||||
return mHeaderHeight;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private float getCurrentScroll() {
|
||||
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;
|
||||
}
|
||||
|
||||
if (mCurrentEmptySpaceView != null
|
||||
&& mCurrentEmptySpaceView.setFixedHeight(mHeaderHeight)) {
|
||||
hasSizeUpdated = true;
|
||||
}
|
||||
return hasSizeUpdated;
|
||||
}
|
||||
|
||||
/** Resets any previous view translation. */
|
||||
public void reset(boolean animate) {
|
||||
if (mOffsetAnimator != null) {
|
||||
mOffsetAnimator.cancel();
|
||||
mOffsetAnimator = null;
|
||||
}
|
||||
|
||||
mScrollOffset = 0;
|
||||
if (!animate) {
|
||||
updateHeaderScroll();
|
||||
} else {
|
||||
float startValue = mLastScroll - getCurrentScroll();
|
||||
mOffsetAnimator = ObjectAnimator.ofFloat(this, SCROLL_OFFSET, startValue, 0);
|
||||
mOffsetAnimator.addListener(forEndCallback(() -> mOffsetAnimator = null));
|
||||
mOffsetAnimator.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if a touch event should be intercepted by this controller.
|
||||
*/
|
||||
public boolean onInterceptTouchEvent(MotionEvent event) {
|
||||
return (mShouldForwardToRecyclerView = proxyMotionEvent(event, INTERCEPT_PROXY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this controller has intercepted and consumed a touch event.
|
||||
*/
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
return mShouldForwardToRecyclerView && proxyMotionEvent(event, TOUCH_PROXY);
|
||||
}
|
||||
|
||||
private boolean proxyMotionEvent(MotionEvent event, MotionEventProxyMethod method) {
|
||||
float dx = mCurrentRecyclerView.getLeft() - mContainer.getLeft();
|
||||
float dy = mCurrentRecyclerView.getTop() - mContainer.getTop();
|
||||
event.offsetLocation(dx, dy);
|
||||
try {
|
||||
return method.proxyEvent(mCurrentRecyclerView, event);
|
||||
} finally {
|
||||
event.offsetLocation(-dx, -dy);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildViewAttachedToWindow(@NonNull View view) {
|
||||
if (view instanceof EmptySpaceView) {
|
||||
findCurrentEmptyView();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildViewDetachedFromWindow(@NonNull View view) {
|
||||
if (view == mCurrentEmptySpaceView) {
|
||||
findCurrentEmptyView();
|
||||
}
|
||||
}
|
||||
|
||||
private void findCurrentEmptyView() {
|
||||
if (mCurrentEmptySpaceView != null) {
|
||||
mCurrentEmptySpaceView.setOnYChangeCallback(null);
|
||||
mCurrentEmptySpaceView = null;
|
||||
}
|
||||
int childCount = mCurrentRecyclerView.getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
View view = mCurrentRecyclerView.getChildAt(i);
|
||||
if (view instanceof EmptySpaceView) {
|
||||
mCurrentEmptySpaceView = (EmptySpaceView) view;
|
||||
mCurrentEmptySpaceView.setFixedHeight(getHeaderHeight());
|
||||
mCurrentEmptySpaceView.setOnYChangeCallback(this::updateHeaderScroll);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private interface MotionEventProxyMethod {
|
||||
|
||||
boolean proxyEvent(ViewGroup view, MotionEvent event);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -62,11 +62,13 @@ import com.android.launcher3.pm.UserCache;
|
||||
import com.android.launcher3.views.ArrowTipView;
|
||||
import com.android.launcher3.views.RecyclerViewFastScroller;
|
||||
import com.android.launcher3.views.SpringRelativeLayout;
|
||||
import com.android.launcher3.views.StickyHeaderLayout;
|
||||
import com.android.launcher3.views.WidgetsEduView;
|
||||
import com.android.launcher3.widget.BaseWidgetSheet;
|
||||
import com.android.launcher3.widget.LauncherAppWidgetHost.ProviderChangedListener;
|
||||
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
|
||||
import com.android.launcher3.widget.picker.search.SearchModeListener;
|
||||
import com.android.launcher3.widget.picker.search.WidgetsSearchBar;
|
||||
import com.android.launcher3.widget.util.WidgetsTableUtils;
|
||||
import com.android.launcher3.workprofile.PersonalWorkPagedView;
|
||||
import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
|
||||
@@ -161,7 +163,13 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
private boolean mIsNoWidgetsViewNeeded;
|
||||
private int mMaxSpansPerRow = DEFAULT_MAX_HORIZONTAL_SPANS;
|
||||
private TextView mNoWidgetsView;
|
||||
private SearchAndRecommendationsScrollController mSearchScrollController;
|
||||
|
||||
private StickyHeaderLayout mSearchScrollView;
|
||||
private WidgetsRecommendationTableLayout mRecommendedWidgetsTable;
|
||||
private View mTabBar;
|
||||
private View mSearchBarContainer;
|
||||
private WidgetsSearchBar mSearchBar;
|
||||
private TextView mHeaderTitle;
|
||||
|
||||
public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
@@ -214,17 +222,23 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
}
|
||||
|
||||
mNoWidgetsView = findViewById(R.id.no_widgets_text);
|
||||
mSearchScrollController = new SearchAndRecommendationsScrollController(
|
||||
findViewById(R.id.search_and_recommendations_container));
|
||||
mSearchScrollController.setCurrentRecyclerView(
|
||||
findViewById(R.id.primary_widgets_list_view));
|
||||
mSearchScrollController.mRecommendedWidgetsTable.setWidgetCellLongClickListener(this);
|
||||
mSearchScrollController.mRecommendedWidgetsTable.setWidgetCellOnClickListener(this);
|
||||
|
||||
mSearchScrollView = findViewById(R.id.search_and_recommendations_container);
|
||||
mSearchScrollView.setCurrentRecyclerView(findViewById(R.id.primary_widgets_list_view));
|
||||
|
||||
mRecommendedWidgetsTable = mSearchScrollView.findViewById(R.id.recommended_widget_table);
|
||||
mRecommendedWidgetsTable.setWidgetCellLongClickListener(this);
|
||||
mRecommendedWidgetsTable.setWidgetCellOnClickListener(this);
|
||||
|
||||
mTabBar = mSearchScrollView.findViewById(R.id.tabs);
|
||||
mSearchBarContainer = mSearchScrollView.findViewById(R.id.search_bar_container);
|
||||
mSearchBar = mSearchScrollView.findViewById(R.id.widgets_search_bar);
|
||||
mHeaderTitle = mSearchScrollView.findViewById(R.id.title);
|
||||
|
||||
onRecommendedWidgetsBound();
|
||||
onWidgetsBound();
|
||||
|
||||
mSearchScrollController.mSearchBar.initialize(
|
||||
mSearchBar.initialize(
|
||||
mActivityContext.getPopupDataProvider(), /* searchModeListener= */ this);
|
||||
|
||||
setUpEducationViewsIfNeeded();
|
||||
@@ -258,7 +272,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
reset();
|
||||
resetExpandedHeaders();
|
||||
mCurrentWidgetsRecyclerView = recyclerView;
|
||||
mSearchScrollController.setCurrentRecyclerView(recyclerView);
|
||||
mSearchScrollView.setCurrentRecyclerView(recyclerView);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,7 +299,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop();
|
||||
}
|
||||
mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop();
|
||||
mSearchScrollController.reset(/* animate= */ true);
|
||||
mSearchScrollView.reset(/* animate= */ true);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -355,8 +369,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
|
||||
@Override
|
||||
protected void onContentHorizontalMarginChanged(int contentHorizontalMarginInPx) {
|
||||
setContentViewChildHorizontalMargin(mSearchScrollController.mContainer,
|
||||
contentHorizontalMarginInPx);
|
||||
setContentViewChildHorizontalMargin(mSearchScrollView, contentHorizontalMarginInPx);
|
||||
if (mViewPager == null) {
|
||||
setContentViewChildHorizontalPadding(
|
||||
mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView,
|
||||
@@ -390,16 +403,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
doMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
if (mSearchScrollController.updateHeaderHeight()) {
|
||||
doMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
if (updateMaxSpansPerRow()) {
|
||||
doMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
if (mSearchScrollController.updateHeaderHeight()) {
|
||||
doMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,7 +465,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
|
||||
if (mHasWorkProfile) {
|
||||
mViewPager.setVisibility(VISIBLE);
|
||||
mSearchScrollController.mTabBar.setVisibility(VISIBLE);
|
||||
mTabBar.setVisibility(VISIBLE);
|
||||
AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK);
|
||||
workUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
|
||||
onActivePageChanged(mViewPager.getCurrentPage());
|
||||
@@ -508,10 +513,10 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
private void setViewVisibilityBasedOnSearch(boolean isInSearchMode) {
|
||||
mIsInSearchMode = isInSearchMode;
|
||||
if (isInSearchMode) {
|
||||
mSearchScrollController.mRecommendedWidgetsTable.setVisibility(GONE);
|
||||
mRecommendedWidgetsTable.setVisibility(GONE);
|
||||
if (mHasWorkProfile) {
|
||||
mViewPager.setVisibility(GONE);
|
||||
mSearchScrollController.mTabBar.setVisibility(GONE);
|
||||
mTabBar.setVisibility(GONE);
|
||||
} else {
|
||||
mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.setVisibility(GONE);
|
||||
}
|
||||
@@ -539,7 +544,6 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
}
|
||||
List<WidgetItem> recommendedWidgets =
|
||||
mActivityContext.getPopupDataProvider().getRecommendedWidgets();
|
||||
WidgetsRecommendationTableLayout table = mSearchScrollController.mRecommendedWidgetsTable;
|
||||
if (recommendedWidgets.size() > 0) {
|
||||
float noWidgetsViewHeight = 0;
|
||||
if (mIsNoWidgetsViewNeeded) {
|
||||
@@ -562,9 +566,10 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
List<ArrayList<WidgetItem>> recommendedWidgetsInTable =
|
||||
WidgetsTableUtils.groupWidgetItemsIntoTableWithoutReordering(
|
||||
recommendedWidgets, mMaxSpansPerRow);
|
||||
table.setRecommendedWidgets(recommendedWidgetsInTable, maxTableHeight);
|
||||
mRecommendedWidgetsTable.setRecommendedWidgets(
|
||||
recommendedWidgetsInTable, maxTableHeight);
|
||||
} else {
|
||||
table.setVisibility(GONE);
|
||||
mRecommendedWidgetsTable.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -619,10 +624,9 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
mNoIntercept = !getRecyclerView().shouldContainerScroll(ev, getPopupContainer());
|
||||
}
|
||||
|
||||
if (mSearchScrollController.mSearchBar.isSearchBarFocused()
|
||||
&& !getPopupContainer().isEventOverView(
|
||||
mSearchScrollController.mSearchBarContainer, ev)) {
|
||||
mSearchScrollController.mSearchBar.clearSearchBarFocus();
|
||||
if (mSearchBar.isSearchBarFocused()
|
||||
&& !getPopupContainer().isEventOverView(mSearchBarContainer, ev)) {
|
||||
mSearchBar.clearSearchBarFocus();
|
||||
}
|
||||
}
|
||||
return super.onControllerInterceptTouchEvent(ev);
|
||||
@@ -663,8 +667,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
|
||||
@Override
|
||||
public int getHeaderViewHeight() {
|
||||
return measureHeightWithVerticalMargins(mSearchScrollController.mHeaderTitle)
|
||||
+ measureHeightWithVerticalMargins(mSearchScrollController.mSearchBarContainer);
|
||||
return measureHeightWithVerticalMargins(mHeaderTitle)
|
||||
+ measureHeightWithVerticalMargins(mSearchBarContainer);
|
||||
}
|
||||
|
||||
/** private the height, in pixel, + the vertical margins of a given view. */
|
||||
@@ -681,14 +685,14 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
protected void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
if (mIsInSearchMode) {
|
||||
mSearchScrollController.mSearchBar.reset();
|
||||
mSearchBar.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed() {
|
||||
if (mIsInSearchMode) {
|
||||
mSearchScrollController.mSearchBar.reset();
|
||||
mSearchBar.reset();
|
||||
return true;
|
||||
}
|
||||
return super.onBackPressed();
|
||||
@@ -701,10 +705,9 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
}
|
||||
|
||||
@Nullable private View getViewToShowEducationTip() {
|
||||
if (mSearchScrollController.mRecommendedWidgetsTable.getVisibility() == VISIBLE
|
||||
&& mSearchScrollController.mRecommendedWidgetsTable.getChildCount() > 0) {
|
||||
return ((ViewGroup) mSearchScrollController.mRecommendedWidgetsTable.getChildAt(0))
|
||||
.getChildAt(0);
|
||||
if (mRecommendedWidgetsTable.getVisibility() == VISIBLE
|
||||
&& mRecommendedWidgetsTable.getChildCount() > 0) {
|
||||
return ((ViewGroup) mRecommendedWidgetsTable.getChildAt(0)).getChildAt(0);
|
||||
}
|
||||
|
||||
AdapterHolder adapterHolder = mAdapters.get(mIsInSearchMode
|
||||
@@ -801,7 +804,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
||||
}
|
||||
|
||||
private int getEmptySpaceHeight() {
|
||||
return mSearchScrollController.getHeaderHeight();
|
||||
return mSearchScrollView.getHeaderHeight();
|
||||
}
|
||||
|
||||
void setup(WidgetsRecyclerView recyclerView) {
|
||||
|
||||
@@ -15,16 +15,12 @@
|
||||
*/
|
||||
package com.android.launcher3.widget.picker;
|
||||
|
||||
import static android.view.View.MeasureSpec.EXACTLY;
|
||||
import static android.view.View.MeasureSpec.makeMeasureSpec;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
|
||||
import com.android.launcher3.recyclerview.ViewHolderBinder;
|
||||
import com.android.launcher3.views.StickyHeaderLayout.EmptySpaceView;
|
||||
import com.android.launcher3.widget.model.WidgetListSpaceEntry;
|
||||
|
||||
import java.util.List;
|
||||
@@ -52,64 +48,4 @@ public class WidgetsSpaceViewHolderBinder
|
||||
@ListPosition int position, List<Object> payloads) {
|
||||
((EmptySpaceView) holder.itemView).setFixedHeight(mEmptySpaceHeightProvider.getAsInt());
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty view which allows listening for 'Y' changes
|
||||
*/
|
||||
public static class EmptySpaceView extends View {
|
||||
|
||||
private Runnable mOnYChangeCallback;
|
||||
private int mHeight = 0;
|
||||
|
||||
private 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user