1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.widget.picker; 18 19 import android.content.Context; 20 import android.graphics.Point; 21 import android.util.AttributeSet; 22 import android.view.MotionEvent; 23 import android.view.View; 24 import android.widget.TableLayout; 25 26 import androidx.recyclerview.widget.LinearLayoutManager; 27 import androidx.recyclerview.widget.RecyclerView; 28 import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; 29 30 import com.android.launcher3.BaseRecyclerView; 31 import com.android.launcher3.DeviceProfile; 32 import com.android.launcher3.R; 33 import com.android.launcher3.views.ActivityContext; 34 import com.android.launcher3.widget.model.WidgetListSpaceEntry; 35 import com.android.launcher3.widget.model.WidgetsListBaseEntry; 36 import com.android.launcher3.widget.model.WidgetsListContentEntry; 37 import com.android.launcher3.widget.model.WidgetsListHeaderEntry; 38 import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry; 39 import com.android.launcher3.widget.picker.WidgetsSpaceViewHolderBinder.EmptySpaceView; 40 41 /** 42 * The widgets recycler view. 43 */ 44 public class WidgetsRecyclerView extends BaseRecyclerView implements OnItemTouchListener { 45 46 private WidgetsListAdapter mAdapter; 47 48 private final int mScrollbarTop; 49 50 private final Point mFastScrollerOffset = new Point(); 51 private boolean mTouchDownOnScroller; 52 private HeaderViewDimensionsProvider mHeaderViewDimensionsProvider; 53 54 // Cached sizes 55 private int mLastVisibleWidgetContentTableHeight = 0; 56 private int mWidgetHeaderHeight = 0; 57 private int mWidgetEmptySpaceHeight = 0; 58 59 private final int mSpacingBetweenEntries; 60 WidgetsRecyclerView(Context context)61 public WidgetsRecyclerView(Context context) { 62 this(context, null); 63 } 64 WidgetsRecyclerView(Context context, AttributeSet attrs)65 public WidgetsRecyclerView(Context context, AttributeSet attrs) { 66 this(context, attrs, 0); 67 } 68 WidgetsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)69 public WidgetsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { 70 // API 21 and below only support 3 parameter ctor. 71 super(context, attrs, defStyleAttr); 72 mScrollbarTop = getResources().getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin); 73 addOnItemTouchListener(this); 74 75 ActivityContext activity = ActivityContext.lookupContext(getContext()); 76 DeviceProfile grid = activity.getDeviceProfile(); 77 78 // The spacing used between entries. 79 mSpacingBetweenEntries = 80 getResources().getDimensionPixelSize(R.dimen.widget_list_entry_spacing); 81 } 82 83 @Override onFinishInflate()84 protected void onFinishInflate() { 85 super.onFinishInflate(); 86 // create a layout manager with Launcher's context so that scroll position 87 // can be preserved during screen rotation. 88 setLayoutManager(new LinearLayoutManager(getContext())); 89 } 90 91 @Override setAdapter(Adapter adapter)92 public void setAdapter(Adapter adapter) { 93 super.setAdapter(adapter); 94 mAdapter = (WidgetsListAdapter) adapter; 95 } 96 97 /** 98 * Maps the touch (from 0..1) to the adapter position that should be visible. 99 */ 100 @Override scrollToPositionAtProgress(float touchFraction)101 public String scrollToPositionAtProgress(float touchFraction) { 102 // Skip early if widgets are not bound. 103 if (isModelNotReady()) { 104 return ""; 105 } 106 107 // Stop the scroller if it is scrolling 108 stopScroll(); 109 110 int rowCount = mAdapter.getItemCount(); 111 float pos = rowCount * touchFraction; 112 int availableScrollHeight = getAvailableScrollHeight(); 113 LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager()); 114 layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); 115 116 int posInt = (int) ((touchFraction == 1) ? pos - 1 : pos); 117 return mAdapter.getSectionName(posInt); 118 } 119 120 /** 121 * Updates the bounds for the scrollbar. 122 */ 123 @Override onUpdateScrollbar(int dy)124 public void onUpdateScrollbar(int dy) { 125 // Skip early if widgets are not bound. 126 if (isModelNotReady()) { 127 mScrollbar.setThumbOffsetY(-1); 128 return; 129 } 130 131 // Skip early if, there no child laid out in the container. 132 int scrollY = getCurrentScrollY(); 133 if (scrollY < 0) { 134 mScrollbar.setThumbOffsetY(-1); 135 return; 136 } 137 138 synchronizeScrollBarThumbOffsetToViewScroll(scrollY, getAvailableScrollHeight()); 139 } 140 141 @Override getCurrentScrollY()142 public int getCurrentScrollY() { 143 // Skip early if widgets are not bound. 144 if (isModelNotReady() || getChildCount() == 0) { 145 return -1; 146 } 147 148 int rowIndex = -1; 149 View child = null; 150 151 LayoutManager layoutManager = getLayoutManager(); 152 if (layoutManager instanceof LinearLayoutManager) { 153 // Use the LayoutManager as the source of truth for visible positions. During 154 // animations, the view group child may not correspond to the visible views that appear 155 // at the top. 156 rowIndex = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); 157 child = layoutManager.findViewByPosition(rowIndex); 158 } 159 160 if (child == null) { 161 // If the layout manager returns null for any reason, which can happen before layout 162 // has occurred for the position, then look at the child of this view as a ViewGroup. 163 child = getChildAt(0); 164 rowIndex = getChildPosition(child); 165 } 166 167 for (int i = 0; i < getChildCount(); i++) { 168 View view = getChildAt(i); 169 if (view instanceof TableLayout) { 170 // This assumes there is ever only one content shown in this recycler view. 171 mLastVisibleWidgetContentTableHeight = view.getMeasuredHeight(); 172 } else if (view instanceof WidgetsListHeader 173 && mWidgetHeaderHeight == 0 174 && view.getMeasuredHeight() > 0) { 175 // This assumes all header views are of the same height. 176 mWidgetHeaderHeight = view.getMeasuredHeight(); 177 } else if (view instanceof EmptySpaceView && view.getMeasuredHeight() > 0) { 178 mWidgetEmptySpaceHeight = view.getMeasuredHeight(); 179 } 180 } 181 182 int scrollPosition = getItemsHeight(rowIndex); 183 int offset = getLayoutManager().getDecoratedTop(child); 184 185 return getPaddingTop() + scrollPosition - offset; 186 } 187 188 /** 189 * Returns the available scroll height, in pixel. 190 * 191 * <p>If the recycler view can't be scrolled, returns 0. 192 */ 193 @Override getAvailableScrollHeight()194 protected int getAvailableScrollHeight() { 195 // AvailableScrollHeight = Total height of the all items - first page height 196 int firstPageHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); 197 int totalHeightOfAllItems = getItemsHeight(/* untilIndex= */ mAdapter.getItemCount()); 198 int availableScrollHeight = totalHeightOfAllItems - firstPageHeight; 199 return Math.max(0, availableScrollHeight); 200 } 201 isModelNotReady()202 private boolean isModelNotReady() { 203 return mAdapter.getItemCount() == 0; 204 } 205 206 @Override getScrollBarTop()207 public int getScrollBarTop() { 208 return mHeaderViewDimensionsProvider == null 209 ? mScrollbarTop 210 : mHeaderViewDimensionsProvider.getHeaderViewHeight() + mScrollbarTop; 211 } 212 213 @Override onInterceptTouchEvent(RecyclerView rv, MotionEvent e)214 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { 215 if (e.getAction() == MotionEvent.ACTION_DOWN) { 216 mTouchDownOnScroller = 217 mScrollbar.isHitInParent(e.getX(), e.getY(), mFastScrollerOffset); 218 } 219 if (mTouchDownOnScroller) { 220 final boolean result = mScrollbar.handleTouchEvent(e, mFastScrollerOffset); 221 return result; 222 } 223 return false; 224 } 225 226 @Override onTouchEvent(RecyclerView rv, MotionEvent e)227 public void onTouchEvent(RecyclerView rv, MotionEvent e) { 228 if (mTouchDownOnScroller) { 229 mScrollbar.handleTouchEvent(e, mFastScrollerOffset); 230 } 231 } 232 233 @Override onRequestDisallowInterceptTouchEvent(boolean disallowIntercept)234 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { 235 } 236 setHeaderViewDimensionsProvider( HeaderViewDimensionsProvider headerViewDimensionsProvider)237 public void setHeaderViewDimensionsProvider( 238 HeaderViewDimensionsProvider headerViewDimensionsProvider) { 239 mHeaderViewDimensionsProvider = headerViewDimensionsProvider; 240 } 241 242 @Override scrollToTop()243 public void scrollToTop() { 244 if (mScrollbar != null) { 245 mScrollbar.reattachThumbToScroll(); 246 } 247 248 if (getLayoutManager() instanceof LinearLayoutManager) { 249 if (getCurrentScrollY() == 0) { 250 // We are at the top, so don't scrollToPosition (would cause unnecessary relayout). 251 return; 252 } 253 } 254 scrollToPosition(0); 255 } 256 257 /** 258 * Returns the sum of the height, in pixels, of this list adapter's items from index 0 until 259 * {@code untilIndex}. 260 * 261 * <p>If the untilIndex is larger than the total number of items in this adapter, returns the 262 * sum of all items' height. 263 */ getItemsHeight(int untilIndex)264 private int getItemsHeight(int untilIndex) { 265 if (untilIndex > mAdapter.getItems().size()) { 266 untilIndex = mAdapter.getItems().size(); 267 } 268 int totalItemsHeight = 0; 269 for (int i = 0; i < untilIndex; i++) { 270 WidgetsListBaseEntry entry = mAdapter.getItems().get(i); 271 if (entry instanceof WidgetsListHeaderEntry 272 || entry instanceof WidgetsListSearchHeaderEntry) { 273 totalItemsHeight += mWidgetHeaderHeight; 274 if (i > 0) { 275 // Each header contains the spacing between entries as top decoration, except 276 // the first one. 277 totalItemsHeight += mSpacingBetweenEntries; 278 } 279 } else if (entry instanceof WidgetsListContentEntry) { 280 totalItemsHeight += mLastVisibleWidgetContentTableHeight; 281 } else if (entry instanceof WidgetListSpaceEntry) { 282 totalItemsHeight += mWidgetEmptySpaceHeight; 283 } else { 284 throw new UnsupportedOperationException("Can't estimate height for " + entry); 285 } 286 } 287 return totalItemsHeight; 288 } 289 290 /** 291 * Provides dimensions of the header view that is shown at the top of a 292 * {@link WidgetsRecyclerView}. 293 */ 294 public interface HeaderViewDimensionsProvider { 295 /** 296 * Returns the height, in pixels, of the header view that is shown at the top of a 297 * {@link WidgetsRecyclerView}. 298 */ getHeaderViewHeight()299 int getHeaderViewHeight(); 300 } 301 } 302