1 /*
2  * Copyright (C) 2017 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.dialer.contactsfragment;
18 
19 import android.content.Context;
20 import android.support.annotation.NonNull;
21 import android.support.v7.widget.LinearLayoutManager;
22 import android.support.v7.widget.RecyclerView;
23 import android.util.AttributeSet;
24 import android.view.MotionEvent;
25 import android.view.View;
26 import android.widget.RelativeLayout;
27 import android.widget.TextView;
28 
29 /** Widget to add fast scrolling to {@link ContactsFragment}. */
30 public class FastScroller extends RelativeLayout {
31 
32   private final int touchTargetWidth;
33 
34   private ContactsAdapter adapter;
35   private LinearLayoutManager layoutManager;
36 
37   private TextView container;
38   private View scrollBar;
39 
40   private boolean dragStarted;
41 
FastScroller(Context context, AttributeSet attrs)42   public FastScroller(Context context, AttributeSet attrs) {
43     super(context, attrs);
44     touchTargetWidth =
45         context.getResources().getDimensionPixelSize(R.dimen.fast_scroller_touch_target_width);
46   }
47 
48   @Override
onFinishInflate()49   protected void onFinishInflate() {
50     super.onFinishInflate();
51     container = findViewById(R.id.fast_scroller_container);
52     scrollBar = findViewById(R.id.fast_scroller_scroll_bar);
53   }
54 
setup(ContactsAdapter adapter, LinearLayoutManager layoutManager)55   void setup(ContactsAdapter adapter, LinearLayoutManager layoutManager) {
56     this.adapter = adapter;
57     this.layoutManager = layoutManager;
58     setVisibility(VISIBLE);
59   }
60 
61   @Override
onTouchEvent(@onNull MotionEvent event)62   public boolean onTouchEvent(@NonNull MotionEvent event) {
63     // Don't override if touch event isn't within desired touch target and dragging hasn't started.
64     if (!dragStarted && getWidth() - touchTargetWidth - event.getX() > 0) {
65       return super.onTouchEvent(event);
66     }
67 
68     switch (event.getAction()) {
69       case MotionEvent.ACTION_DOWN:
70         dragStarted = true;
71         container.setVisibility(VISIBLE);
72         scrollBar.setSelected(true);
73         // fall through
74       case MotionEvent.ACTION_MOVE:
75         setContainerAndScrollBarPosition(event.getY());
76         setRecyclerViewPosition(event.getY());
77         return true;
78       case MotionEvent.ACTION_UP:
79       case MotionEvent.ACTION_CANCEL:
80         dragStarted = false;
81         container.setVisibility(INVISIBLE);
82         scrollBar.setSelected(false);
83         return true;
84       default:
85         return super.onTouchEvent(event);
86     }
87   }
88 
isDragStarted()89   public boolean isDragStarted() {
90     return dragStarted;
91   }
92 
setRecyclerViewPosition(float y)93   private void setRecyclerViewPosition(float y) {
94     final int itemCount = adapter.getItemCount();
95     float scrolledPosition = getScrolledPercentage(y) * (float) itemCount;
96     int targetPos = getValueInRange(0, itemCount - 1, (int) scrolledPosition);
97     layoutManager.scrollToPositionWithOffset(targetPos, 0);
98     container.setText(adapter.getHeaderString(targetPos));
99     adapter.refreshHeaders();
100   }
101 
102   // Returns a float in range [0, 1] which represents the position of the scroller.
getScrolledPercentage(float y)103   private float getScrolledPercentage(float y) {
104     if (scrollBar.getY() == 0) {
105       return 0f;
106     } else if (scrollBar.getY() + scrollBar.getHeight() >= getHeight()) {
107       return 1f;
108     } else {
109       return y / (float) getHeight();
110     }
111   }
112 
getValueInRange(int min, int max, int value)113   private int getValueInRange(int min, int max, int value) {
114     int minimum = Math.max(min, value);
115     return Math.min(minimum, max);
116   }
117 
updateContainerAndScrollBarPosition(RecyclerView recyclerView)118   void updateContainerAndScrollBarPosition(RecyclerView recyclerView) {
119     if (!scrollBar.isSelected()) {
120       int verticalScrollOffset = recyclerView.computeVerticalScrollOffset();
121       int verticalScrollRange = recyclerView.computeVerticalScrollRange();
122       float proportion = (float) verticalScrollOffset / ((float) verticalScrollRange - getHeight());
123       setContainerAndScrollBarPosition(getHeight() * proportion);
124     }
125   }
126 
setContainerAndScrollBarPosition(float y)127   private void setContainerAndScrollBarPosition(float y) {
128     int scrollBarHeight = scrollBar.getHeight();
129     int containerHeight = container.getHeight();
130     scrollBar.setY(
131         getValueInRange(0, getHeight() - scrollBarHeight, (int) (y - scrollBarHeight / 2)));
132     container.setY(
133         getValueInRange(
134             0, getHeight() - containerHeight - scrollBarHeight / 2, (int) (y - containerHeight)));
135   }
136 }
137