blob: b942ea4516958315745e20b02e6052a6f35e507a [file] [log] [blame]
Winson Chung93f98ea2015-03-10 16:28:47 -07001/*
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 */
16package com.android.launcher3;
17
18import android.animation.ObjectAnimator;
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.Paint;
24import android.graphics.Rect;
25import android.graphics.drawable.Drawable;
Winson Chung24cf7002015-03-30 14:25:04 -070026import android.support.v7.widget.LinearLayoutManager;
Winson Chung93f98ea2015-03-10 16:28:47 -070027import android.support.v7.widget.RecyclerView;
28import android.util.AttributeSet;
29import android.view.MotionEvent;
Winson Chungf819dc22015-03-23 14:45:54 -070030import android.view.View;
Winson Chung93f98ea2015-03-10 16:28:47 -070031import android.view.ViewConfiguration;
32
33import java.util.List;
34
35/**
36 * A RecyclerView with custom fastscroll support. This is the main container for the all apps
37 * icons.
38 */
39public class AppsContainerRecyclerView extends RecyclerView
40 implements RecyclerView.OnItemTouchListener {
41
Winson Chungaa2ab252015-03-16 12:39:05 -070042 private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f;
43
Winson Chung93f98ea2015-03-10 16:28:47 -070044 private AlphabeticalAppsList mApps;
45 private int mNumAppsPerRow;
46
Winson Chungf819dc22015-03-23 14:45:54 -070047 private Drawable mScrollbar;
Winson Chung93f98ea2015-03-10 16:28:47 -070048 private Drawable mFastScrollerBg;
Winson Chungf819dc22015-03-23 14:45:54 -070049 private Rect mVerticalScrollbarBounds = new Rect();
Winson Chung93f98ea2015-03-10 16:28:47 -070050 private boolean mDraggingFastScroller;
51 private String mFastScrollSectionName;
52 private Paint mFastScrollTextPaint;
53 private Rect mFastScrollTextBounds = new Rect();
54 private float mFastScrollAlpha;
55 private int mDownX;
56 private int mDownY;
57 private int mLastX;
58 private int mLastY;
Winson Chungf819dc22015-03-23 14:45:54 -070059 private int mScrollbarWidth;
60 private int mScrollbarMinHeight;
61 private int mScrollbarInset;
Winson Chung93f98ea2015-03-10 16:28:47 -070062
63 public AppsContainerRecyclerView(Context context) {
64 this(context, null);
65 }
66
67 public AppsContainerRecyclerView(Context context, AttributeSet attrs) {
68 this(context, attrs, 0);
69 }
70
71 public AppsContainerRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
72 this(context, attrs, defStyleAttr, 0);
73 }
74
75 public AppsContainerRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
76 int defStyleRes) {
77 super(context, attrs, defStyleAttr);
78
79 Resources res = context.getResources();
80 int fastScrollerSize = res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_popup_size);
Winson Chung3d9490a2015-03-24 17:38:42 -070081 mScrollbar = res.getDrawable(R.drawable.apps_list_scrollbar_thumb);
82 mFastScrollerBg = res.getDrawable(R.drawable.apps_list_fastscroll_bg);
Winson Chung93f98ea2015-03-10 16:28:47 -070083 mFastScrollerBg.setBounds(0, 0, fastScrollerSize, fastScrollerSize);
84 mFastScrollTextPaint = new Paint();
85 mFastScrollTextPaint.setColor(Color.WHITE);
86 mFastScrollTextPaint.setAntiAlias(true);
87 mFastScrollTextPaint.setTextSize(res.getDimensionPixelSize(
88 R.dimen.apps_view_fast_scroll_text_size));
Winson Chungf819dc22015-03-23 14:45:54 -070089 mScrollbarWidth = res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_bar_width);
90 mScrollbarMinHeight =
91 res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_bar_min_height);
92 mScrollbarInset =
93 res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_scrubber_touch_inset);
Winson Chung93f98ea2015-03-10 16:28:47 -070094 setFastScrollerAlpha(getFastScrollerAlpha());
95 }
96
97 /**
98 * Sets the list of apps in this view, used to determine the fastscroll position.
99 */
100 public void setApps(AlphabeticalAppsList apps) {
101 mApps = apps;
102 }
103
104 /**
105 * Sets the number of apps per row in this recycler view.
106 */
107 public void setNumAppsPerRow(int rowSize) {
108 mNumAppsPerRow = rowSize;
109 }
110
111 /**
112 * Sets the fast scroller alpha.
113 */
114 public void setFastScrollerAlpha(float alpha) {
115 mFastScrollAlpha = alpha;
116 invalidateFastScroller();
117 }
118
119 /**
120 * Gets the fast scroller alpha.
121 */
122 public float getFastScrollerAlpha() {
123 return mFastScrollAlpha;
124 }
125
Winson Chungf819dc22015-03-23 14:45:54 -0700126 /**
127 * Returns the scroll bar width.
128 */
129 public int getScrollbarWidth() {
130 return mScrollbarWidth;
131 }
132
Winson Chung93f98ea2015-03-10 16:28:47 -0700133 @Override
134 protected void onFinishInflate() {
135 addOnItemTouchListener(this);
136 }
137
138 @Override
139 protected void dispatchDraw(Canvas canvas) {
140 super.dispatchDraw(canvas);
Winson Chungf819dc22015-03-23 14:45:54 -0700141 drawVerticalScrubber(canvas);
142 drawFastScrollerPopup(canvas);
Winson Chung93f98ea2015-03-10 16:28:47 -0700143 }
144
145 /**
146 * We intercept the touch handling only to support fast scrolling when initiated from the
Winson Chungf819dc22015-03-23 14:45:54 -0700147 * scroll bar. Otherwise, we fall back to the default RecyclerView touch handling.
Winson Chung93f98ea2015-03-10 16:28:47 -0700148 */
149 @Override
150 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
151 return handleTouchEvent(ev);
152 }
153
154 @Override
155 public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
156 handleTouchEvent(ev);
157 }
158
159 /**
160 * Handles the touch event and determines whether to show the fast scroller (or updates it if
161 * it is already showing).
162 */
163 private boolean handleTouchEvent(MotionEvent ev) {
164 ViewConfiguration config = ViewConfiguration.get(getContext());
165
166 int action = ev.getAction();
167 int x = (int) ev.getX();
168 int y = (int) ev.getY();
169 switch (action) {
170 case MotionEvent.ACTION_DOWN:
171 // Keep track of the down positions
172 mDownX = mLastX = x;
173 mDownY = mLastY = y;
174 stopScroll();
175 break;
176 case MotionEvent.ACTION_MOVE:
177 // Check if we are scrolling
Winson Chungf819dc22015-03-23 14:45:54 -0700178 if (!mDraggingFastScroller && isPointNearScrollbar(mDownX, mDownY) &&
Winson Chung93f98ea2015-03-10 16:28:47 -0700179 Math.abs(y - mDownY) > config.getScaledTouchSlop()) {
180 getParent().requestDisallowInterceptTouchEvent(true);
181 mDraggingFastScroller = true;
182 animateFastScrollerVisibility(true);
183 }
184 if (mDraggingFastScroller) {
185 mLastX = x;
186 mLastY = y;
187
188 // Scroll to the right position, and update the section name
189 int top = getPaddingTop() + (mFastScrollerBg.getBounds().height() / 2);
190 int bottom = getHeight() - getPaddingBottom() -
191 (mFastScrollerBg.getBounds().height() / 2);
192 float boundedY = (float) Math.max(top, Math.min(bottom, y));
193 mFastScrollSectionName = scrollToPositionAtProgress((boundedY - top) /
194 (bottom - top));
195 invalidateFastScroller();
196 }
197 break;
198 case MotionEvent.ACTION_UP:
199 case MotionEvent.ACTION_CANCEL:
200 mDraggingFastScroller = false;
201 animateFastScrollerVisibility(false);
202 break;
203 }
204 return mDraggingFastScroller;
205
206 }
207
208 /**
209 * Animates the visibility of the fast scroller popup.
210 */
211 private void animateFastScrollerVisibility(boolean visible) {
212 ObjectAnimator anim = ObjectAnimator.ofFloat(this, "fastScrollerAlpha", visible ? 1f : 0f);
213 anim.setDuration(visible ? 200 : 150);
214 anim.start();
215 }
216
217 /**
Winson Chungf819dc22015-03-23 14:45:54 -0700218 * Returns whether a given point is near the scrollbar.
219 */
220 private boolean isPointNearScrollbar(int x, int y) {
221 // Check if we are scrolling
222 updateVerticalScrollbarBounds();
223 mVerticalScrollbarBounds.inset(mScrollbarInset, mScrollbarInset);
224 return mVerticalScrollbarBounds.contains(x, y);
225 }
226
227 /**
228 * Draws the fast scroller popup.
229 */
230 private void drawFastScrollerPopup(Canvas canvas) {
231 int x;
232 int y;
233 boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
234 LAYOUT_DIRECTION_RTL);
235
236 if (mFastScrollAlpha > 0f) {
237 // Calculate the position for the fast scroller popup
238 Rect bgBounds = mFastScrollerBg.getBounds();
239 if (isRtl) {
240 x = getPaddingLeft() + getScrollBarSize();
241 } else {
242 x = getWidth() - getPaddingRight() - getScrollBarSize() - bgBounds.width();
243 }
244 y = mLastY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgBounds.height());
245 y = Math.max(getPaddingTop(), Math.min(y, getHeight() - getPaddingBottom() -
246 bgBounds.height()));
247
248 // Draw the fast scroller popup
249 int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
250 canvas.translate(x, y);
251 mFastScrollerBg.setAlpha((int) (mFastScrollAlpha * 255));
252 mFastScrollerBg.draw(canvas);
253 mFastScrollTextPaint.setAlpha((int) (mFastScrollAlpha * 255));
254 mFastScrollTextPaint.getTextBounds(mFastScrollSectionName, 0,
255 mFastScrollSectionName.length(), mFastScrollTextBounds);
256 canvas.drawText(mFastScrollSectionName,
257 (bgBounds.width() - mFastScrollTextBounds.width()) / 2,
258 bgBounds.height() - (bgBounds.height() - mFastScrollTextBounds.height()) / 2,
259 mFastScrollTextPaint);
260 canvas.restoreToCount(restoreCount);
261 }
262 }
263
264 /**
265 * Draws the vertical scrollbar.
266 */
267 private void drawVerticalScrubber(Canvas canvas) {
268 updateVerticalScrollbarBounds();
269
270 // Draw the scroll bar
271 int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
272 canvas.translate(mVerticalScrollbarBounds.left, mVerticalScrollbarBounds.top);
273 mScrollbar.setBounds(0, 0, mScrollbarWidth, mVerticalScrollbarBounds.height());
274 mScrollbar.draw(canvas);
275 canvas.restoreToCount(restoreCount);
276 }
277
278 /**
Winson Chung93f98ea2015-03-10 16:28:47 -0700279 * Invalidates the fast scroller popup.
280 */
281 private void invalidateFastScroller() {
282 invalidate(getWidth() - getPaddingRight() - getScrollBarSize() -
283 mFastScrollerBg.getIntrinsicWidth(), 0, getWidth(), getHeight());
284 }
285
286 /**
287 * Maps the progress (from 0..1) to the position that should be visible
288 */
289 private String scrollToPositionAtProgress(float progress) {
290 List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections();
291 // Get the total number of rows
Winson Chungf819dc22015-03-23 14:45:54 -0700292 int rowCount = getNumRows();
Winson Chung93f98ea2015-03-10 16:28:47 -0700293
Winson Chung24cf7002015-03-30 14:25:04 -0700294 // Find the position of the first application in the section that contains the row at the
295 // current progress
Winson Chung93f98ea2015-03-10 16:28:47 -0700296 int rowAtProgress = (int) (progress * rowCount);
297 int appIndex = 0;
298 rowCount = 0;
299 for (AlphabeticalAppsList.SectionInfo info : sections) {
300 int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow);
301 if (rowCount + numRowsInSection > rowAtProgress) {
Winson Chung93f98ea2015-03-10 16:28:47 -0700302 break;
303 }
304 rowCount += numRowsInSection;
305 appIndex += info.numAppsInSection;
306 }
307 appIndex = Math.max(0, Math.min(mApps.getAppsWithoutSectionBreaks().size() - 1, appIndex));
308 AppInfo appInfo = mApps.getAppsWithoutSectionBreaks().get(appIndex);
309 int sectionedAppIndex = mApps.getApps().indexOf(appInfo);
Winson Chung93f98ea2015-03-10 16:28:47 -0700310
Winson Chung24cf7002015-03-30 14:25:04 -0700311 // Scroll the position into view, anchored at the top of the screen if possible. We call the
312 // scroll method on the LayoutManager directly since it is not exposed by RecyclerView.
313 LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
314 stopScroll();
315 layoutManager.scrollToPositionWithOffset(sectionedAppIndex, 0);
316
317 // Return the section name of the row
Winson Chung93f98ea2015-03-10 16:28:47 -0700318 return mApps.getSectionNameForApp(appInfo);
319 }
Winson Chungf819dc22015-03-23 14:45:54 -0700320
321 /**
322 * Returns the bounds for the scrollbar.
323 */
324 private void updateVerticalScrollbarBounds() {
325 int x;
326 int y;
327 boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
328 LAYOUT_DIRECTION_RTL);
329
330 // Skip early if there are no items
331 if (mApps.getApps().isEmpty()) {
332 mVerticalScrollbarBounds.setEmpty();
333 return;
334 }
335
336 // Find the index and height of the first visible row (all rows have the same height)
337 int rowIndex = -1;
338 int rowTopOffset = -1;
339 int rowHeight = -1;
340 int rowCount = getNumRows();
341 int childCount = getChildCount();
342 for (int i = 0; i < childCount; i++) {
343 View child = getChildAt(i);
344 int position = getChildPosition(child);
345 if (position != NO_POSITION) {
346 AppInfo info = mApps.getApps().get(position);
347 if (info != AlphabeticalAppsList.SECTION_BREAK_INFO) {
348 int appIndex = mApps.getAppsWithoutSectionBreaks().indexOf(info);
349 rowIndex = findRowForAppIndex(appIndex);
350 rowTopOffset = getLayoutManager().getDecoratedTop(child);
351 rowHeight = child.getHeight();
352 break;
353 }
354 }
355 }
356
357 if (rowIndex != -1) {
358 int height = getHeight() - getPaddingTop() - getPaddingBottom();
359 int totalScrollHeight = rowCount * rowHeight;
360 if (totalScrollHeight > height) {
361 int scrollbarHeight = Math.max(mScrollbarMinHeight,
362 (int) (height / ((float) totalScrollHeight / height)));
363
364 // Calculate the position and size of the scroll bar
365 if (isRtl) {
366 x = getPaddingLeft();
367 } else {
368 x = getWidth() - getPaddingRight() - mScrollbarWidth;
369 }
370
371 // To calculate the offset, we compute the percentage of the total scrollable height
372 // that the user has already scrolled and then map that to the scroll bar bounds
373 int availableY = totalScrollHeight - height;
374 int availableScrollY = height - scrollbarHeight;
375 y = (rowIndex * rowHeight) - rowTopOffset;
376 y = getPaddingTop() +
377 (int) (((float) (getPaddingTop() + y) / availableY) * availableScrollY);
378
379 mVerticalScrollbarBounds.set(x, y, x + mScrollbarWidth, y + scrollbarHeight);
380 return;
381 }
382 }
383 mVerticalScrollbarBounds.setEmpty();
384 }
385
386 /**
387 * Returns the row index for a given position in the list.
388 */
389 private int findRowForAppIndex(int position) {
390 List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections();
391 int appIndex = 0;
392 int rowCount = 0;
393 for (AlphabeticalAppsList.SectionInfo info : sections) {
394 int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow);
395 if (appIndex + info.numAppsInSection > position) {
396 return rowCount + ((position - appIndex) / mNumAppsPerRow);
397 }
398 appIndex += info.numAppsInSection;
399 rowCount += numRowsInSection;
400 }
401 return appIndex;
402 }
403
404 /**
405 * Returns the total number of rows in the list.
406 */
407 private int getNumRows() {
408 List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections();
409 int rowCount = 0;
410 for (AlphabeticalAppsList.SectionInfo info : sections) {
411 int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow);
412 rowCount += numRowsInSection;
413 }
414 return rowCount;
415 }
Winson Chung93f98ea2015-03-10 16:28:47 -0700416}