/* * Copyright (C) 2015 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.graphics.Point; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.TableLayout; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; import com.android.launcher3.BaseRecyclerView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.widget.model.WidgetListSpaceEntry; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.model.WidgetsListContentEntry; import com.android.launcher3.widget.model.WidgetsListHeaderEntry; import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry; import com.android.launcher3.widget.picker.WidgetsSpaceViewHolderBinder.EmptySpaceView; /** * The widgets recycler view. */ public class WidgetsRecyclerView extends BaseRecyclerView implements OnItemTouchListener { private WidgetsListAdapter mAdapter; private final int mScrollbarTop; private final Point mFastScrollerOffset = new Point(); private boolean mTouchDownOnScroller; private HeaderViewDimensionsProvider mHeaderViewDimensionsProvider; // Cached sizes private int mLastVisibleWidgetContentTableHeight = 0; private int mWidgetHeaderHeight = 0; private int mWidgetEmptySpaceHeight = 0; private final int mSpacingBetweenEntries; public WidgetsRecyclerView(Context context) { this(context, null); } public WidgetsRecyclerView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public WidgetsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { // API 21 and below only support 3 parameter ctor. super(context, attrs, defStyleAttr); mScrollbarTop = getResources().getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin); addOnItemTouchListener(this); ActivityContext activity = ActivityContext.lookupContext(getContext()); DeviceProfile grid = activity.getDeviceProfile(); // The spacing used between entries. mSpacingBetweenEntries = getResources().getDimensionPixelSize(R.dimen.widget_list_entry_spacing); } @Override protected void onFinishInflate() { super.onFinishInflate(); // create a layout manager with Launcher's context so that scroll position // can be preserved during screen rotation. setLayoutManager(new LinearLayoutManager(getContext())); } @Override public void setAdapter(Adapter adapter) { super.setAdapter(adapter); mAdapter = (WidgetsListAdapter) adapter; } /** * Maps the touch (from 0..1) to the adapter position that should be visible. */ @Override public String scrollToPositionAtProgress(float touchFraction) { // Skip early if widgets are not bound. if (isModelNotReady()) { return ""; } // Stop the scroller if it is scrolling stopScroll(); int rowCount = mAdapter.getItemCount(); float pos = rowCount * touchFraction; int availableScrollHeight = getAvailableScrollHeight(); LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager()); layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); int posInt = (int) ((touchFraction == 1) ? pos - 1 : pos); return mAdapter.getSectionName(posInt); } /** * Updates the bounds for the scrollbar. */ @Override public void onUpdateScrollbar(int dy) { // Skip early if widgets are not bound. if (isModelNotReady()) { mScrollbar.setThumbOffsetY(-1); return; } // Skip early if, there no child laid out in the container. int scrollY = getCurrentScrollY(); if (scrollY < 0) { mScrollbar.setThumbOffsetY(-1); return; } synchronizeScrollBarThumbOffsetToViewScroll(scrollY, getAvailableScrollHeight()); } @Override public int getCurrentScrollY() { // Skip early if widgets are not bound. if (isModelNotReady() || getChildCount() == 0) { return -1; } int rowIndex = -1; View child = null; LayoutManager layoutManager = getLayoutManager(); if (layoutManager instanceof LinearLayoutManager) { // Use the LayoutManager as the source of truth for visible positions. During // animations, the view group child may not correspond to the visible views that appear // at the top. rowIndex = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); child = layoutManager.findViewByPosition(rowIndex); } if (child == null) { // If the layout manager returns null for any reason, which can happen before layout // has occurred for the position, then look at the child of this view as a ViewGroup. child = getChildAt(0); rowIndex = getChildPosition(child); } for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); if (view instanceof TableLayout) { // This assumes there is ever only one content shown in this recycler view. mLastVisibleWidgetContentTableHeight = view.getMeasuredHeight(); } else if (view instanceof WidgetsListHeader && mWidgetHeaderHeight == 0 && view.getMeasuredHeight() > 0) { // This assumes all header views are of the same height. mWidgetHeaderHeight = view.getMeasuredHeight(); } else if (view instanceof EmptySpaceView && view.getMeasuredHeight() > 0) { mWidgetEmptySpaceHeight = view.getMeasuredHeight(); } } int scrollPosition = getItemsHeight(rowIndex); int offset = getLayoutManager().getDecoratedTop(child); return getPaddingTop() + scrollPosition - offset; } /** * Returns the available scroll height, in pixel. * *

If the recycler view can't be scrolled, returns 0. */ @Override protected int getAvailableScrollHeight() { // AvailableScrollHeight = Total height of the all items - first page height int firstPageHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); int totalHeightOfAllItems = getItemsHeight(/* untilIndex= */ mAdapter.getItemCount()); int availableScrollHeight = totalHeightOfAllItems - firstPageHeight; return Math.max(0, availableScrollHeight); } private boolean isModelNotReady() { return mAdapter.getItemCount() == 0; } @Override public int getScrollBarTop() { return mHeaderViewDimensionsProvider == null ? mScrollbarTop : mHeaderViewDimensionsProvider.getHeaderViewHeight() + mScrollbarTop; } @Override public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { if (e.getAction() == MotionEvent.ACTION_DOWN) { mTouchDownOnScroller = mScrollbar.isHitInParent(e.getX(), e.getY(), mFastScrollerOffset); } if (mTouchDownOnScroller) { final boolean result = mScrollbar.handleTouchEvent(e, mFastScrollerOffset); return result; } return false; } @Override public void onTouchEvent(RecyclerView rv, MotionEvent e) { if (mTouchDownOnScroller) { mScrollbar.handleTouchEvent(e, mFastScrollerOffset); } } @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { } public void setHeaderViewDimensionsProvider( HeaderViewDimensionsProvider headerViewDimensionsProvider) { mHeaderViewDimensionsProvider = headerViewDimensionsProvider; } @Override public void scrollToTop() { if (mScrollbar != null) { mScrollbar.reattachThumbToScroll(); } if (getLayoutManager() instanceof LinearLayoutManager) { if (getCurrentScrollY() == 0) { // We are at the top, so don't scrollToPosition (would cause unnecessary relayout). return; } } scrollToPosition(0); } /** * Returns the sum of the height, in pixels, of this list adapter's items from index 0 until * {@code untilIndex}. * *

If the untilIndex is larger than the total number of items in this adapter, returns the * sum of all items' height. */ private int getItemsHeight(int untilIndex) { if (untilIndex > mAdapter.getItems().size()) { untilIndex = mAdapter.getItems().size(); } int totalItemsHeight = 0; for (int i = 0; i < untilIndex; i++) { WidgetsListBaseEntry entry = mAdapter.getItems().get(i); if (entry instanceof WidgetsListHeaderEntry || entry instanceof WidgetsListSearchHeaderEntry) { totalItemsHeight += mWidgetHeaderHeight; if (i > 0) { // Each header contains the spacing between entries as top decoration, except // the first one. totalItemsHeight += mSpacingBetweenEntries; } } else if (entry instanceof WidgetsListContentEntry) { totalItemsHeight += mLastVisibleWidgetContentTableHeight; } else if (entry instanceof WidgetListSpaceEntry) { totalItemsHeight += mWidgetEmptySpaceHeight; } else { throw new UnsupportedOperationException("Can't estimate height for " + entry); } } return totalItemsHeight; } /** * Provides dimensions of the header view that is shown at the top of a * {@link WidgetsRecyclerView}. */ public interface HeaderViewDimensionsProvider { /** * Returns the height, in pixels, of the header view that is shown at the top of a * {@link WidgetsRecyclerView}. */ int getHeaderViewHeight(); } }