1/* 2 * Copyright (C) 2016 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 17package android.support.wear.widget; 18 19import android.annotation.TargetApi; 20import android.content.Context; 21import android.content.res.TypedArray; 22import android.graphics.Point; 23import android.os.Build; 24import android.support.annotation.Nullable; 25import android.support.v7.widget.RecyclerView; 26import android.support.wear.R; 27import android.util.AttributeSet; 28import android.view.MotionEvent; 29import android.view.View; 30import android.view.ViewTreeObserver; 31 32/** 33 * Wearable specific implementation of the {@link RecyclerView} enabling {@link 34 * #setCircularScrollingGestureEnabled(boolean)} circular scrolling} and semi-circular layouts. 35 * 36 * @see #setCircularScrollingGestureEnabled(boolean) 37 */ 38@TargetApi(Build.VERSION_CODES.M) 39public class WearableRecyclerView extends RecyclerView { 40 private static final String TAG = "WearableRecyclerView"; 41 42 private static final int NO_VALUE = Integer.MIN_VALUE; 43 44 private final ScrollManager mScrollManager = new ScrollManager(); 45 private boolean mCircularScrollingEnabled; 46 private boolean mEdgeItemsCenteringEnabled; 47 private boolean mCenterEdgeItemsWhenThereAreChildren; 48 49 private int mOriginalPaddingTop = NO_VALUE; 50 private int mOriginalPaddingBottom = NO_VALUE; 51 52 /** Pre-draw listener which is used to adjust the padding on this view before its first draw. */ 53 private final ViewTreeObserver.OnPreDrawListener mPaddingPreDrawListener = 54 new ViewTreeObserver.OnPreDrawListener() { 55 @Override 56 public boolean onPreDraw() { 57 if (mCenterEdgeItemsWhenThereAreChildren && getChildCount() > 0) { 58 setupCenteredPadding(); 59 mCenterEdgeItemsWhenThereAreChildren = false; 60 } 61 return true; 62 } 63 }; 64 65 public WearableRecyclerView(Context context) { 66 this(context, null); 67 } 68 69 public WearableRecyclerView(Context context, @Nullable AttributeSet attrs) { 70 this(context, attrs, 0); 71 } 72 73 public WearableRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { 74 this(context, attrs, defStyle, 0); 75 } 76 77 public WearableRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle, 78 int defStyleRes) { 79 super(context, attrs, defStyle); 80 81 setHasFixedSize(true); 82 // Padding is used to center the top and bottom items in the list, don't clip to padding to 83 // allows the items to draw in that space. 84 setClipToPadding(false); 85 86 if (attrs != null) { 87 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WearableRecyclerView, 88 defStyle, defStyleRes); 89 90 setCircularScrollingGestureEnabled( 91 a.getBoolean( 92 R.styleable.WearableRecyclerView_circularScrollingGestureEnabled, 93 mCircularScrollingEnabled)); 94 setBezelFraction( 95 a.getFloat(R.styleable.WearableRecyclerView_bezelWidth, 96 mScrollManager.getBezelWidth())); 97 setScrollDegreesPerScreen( 98 a.getFloat( 99 R.styleable.WearableRecyclerView_scrollDegreesPerScreen, 100 mScrollManager.getScrollDegreesPerScreen())); 101 a.recycle(); 102 } 103 } 104 105 private void setupCenteredPadding() { 106 if (getChildCount() < 1 || !mEdgeItemsCenteringEnabled) { 107 return; 108 } 109 // All the children in the view are the same size, as we set setHasFixedSize 110 // to true, so the height of the first child is the same as all of them. 111 View child = getChildAt(0); 112 int height = child.getHeight(); 113 // This is enough padding to center the child view in the parent. 114 int desiredPadding = (int) ((getHeight() * 0.5f) - (height * 0.5f)); 115 116 if (getPaddingTop() != desiredPadding) { 117 mOriginalPaddingTop = getPaddingTop(); 118 mOriginalPaddingBottom = getPaddingBottom(); 119 // The view is symmetric along the vertical axis, so the top and bottom 120 // can be the same. 121 setPadding(getPaddingLeft(), desiredPadding, getPaddingRight(), desiredPadding); 122 123 // The focused child should be in the center, so force a scroll to it. 124 View focusedChild = getFocusedChild(); 125 int focusedPosition = 126 (focusedChild != null) ? getLayoutManager().getPosition( 127 focusedChild) : 0; 128 getLayoutManager().scrollToPosition(focusedPosition); 129 } 130 } 131 132 private void setupOriginalPadding() { 133 if (mOriginalPaddingTop == NO_VALUE) { 134 return; 135 } else { 136 setPadding(getPaddingLeft(), mOriginalPaddingTop, getPaddingRight(), 137 mOriginalPaddingBottom); 138 } 139 } 140 141 @Override 142 public boolean onTouchEvent(MotionEvent event) { 143 if (mCircularScrollingEnabled && mScrollManager.onTouchEvent(event)) { 144 return true; 145 } 146 return super.onTouchEvent(event); 147 } 148 149 @Override 150 protected void onAttachedToWindow() { 151 super.onAttachedToWindow(); 152 Point screenSize = new Point(); 153 getDisplay().getSize(screenSize); 154 mScrollManager.setRecyclerView(this, screenSize.x, screenSize.y); 155 getViewTreeObserver().addOnPreDrawListener(mPaddingPreDrawListener); 156 } 157 158 @Override 159 protected void onDetachedFromWindow() { 160 super.onDetachedFromWindow(); 161 mScrollManager.clearRecyclerView(); 162 getViewTreeObserver().removeOnPreDrawListener(mPaddingPreDrawListener); 163 } 164 165 /** 166 * Enables/disables circular touch scrolling for this view. When enabled, circular touch 167 * gestures around the edge of the screen will cause the view to scroll up or down. Related 168 * methods let you specify the characteristics of the scrolling, like the speed of the scroll 169 * or the are considered for the start of this scrolling gesture. 170 * 171 * @see #setScrollDegreesPerScreen(float) 172 * @see #setBezelFraction(float) 173 */ 174 public void setCircularScrollingGestureEnabled(boolean circularScrollingGestureEnabled) { 175 mCircularScrollingEnabled = circularScrollingGestureEnabled; 176 } 177 178 /** 179 * Returns whether circular scrolling is enabled for this view. 180 * 181 * @see #setCircularScrollingGestureEnabled(boolean) 182 */ 183 public boolean isCircularScrollingGestureEnabled() { 184 return mCircularScrollingEnabled; 185 } 186 187 /** 188 * Sets how many degrees the user has to rotate by to scroll through one screen height when they 189 * are using the circular scrolling gesture.The default value equates 180 degrees scroll to one 190 * screen. 191 * 192 * @see #setCircularScrollingGestureEnabled(boolean) 193 * 194 * @param degreesPerScreen the number of degrees to rotate by to scroll through one whole 195 * height of the screen, 196 */ 197 public void setScrollDegreesPerScreen(float degreesPerScreen) { 198 mScrollManager.setScrollDegreesPerScreen(degreesPerScreen); 199 } 200 201 /** 202 * Returns how many degrees does the user have to rotate for to scroll through one screen 203 * height. 204 * 205 * @see #setCircularScrollingGestureEnabled(boolean) 206 * @see #setScrollDegreesPerScreen(float). 207 */ 208 public float getScrollDegreesPerScreen() { 209 return mScrollManager.getScrollDegreesPerScreen(); 210 } 211 212 /** 213 * Taps within this radius and the radius of the screen are considered close enough to the 214 * bezel to be candidates for circular scrolling. Expressed as a fraction of the screen's 215 * radius. The default is the whole screen i.e 1.0f. 216 */ 217 public void setBezelFraction(float fraction) { 218 mScrollManager.setBezelWidth(fraction); 219 } 220 221 /** 222 * Returns the current bezel width for circular scrolling as a fraction of the screen's 223 * radius. 224 * 225 * @see #setBezelFraction(float) 226 */ 227 public float getBezelFraction() { 228 return mScrollManager.getBezelWidth(); 229 } 230 231 /** 232 * Use this method to configure the {@link WearableRecyclerView} to always align the first and 233 * last items with the vertical center of the screen. This effectively moves the start and end 234 * of the list to the middle of the screen if the user has scrolled so far. It takes the height 235 * of the children into account so that they are correctly centered. 236 * 237 * @param isEnabled set to true if you wish to align the edge children (first and last) 238 * with the center of the screen. 239 */ 240 public void setEdgeItemsCenteringEnabled(boolean isEnabled) { 241 mEdgeItemsCenteringEnabled = isEnabled; 242 if (mEdgeItemsCenteringEnabled) { 243 if (getChildCount() > 0) { 244 setupCenteredPadding(); 245 } else { 246 mCenterEdgeItemsWhenThereAreChildren = true; 247 } 248 } else { 249 setupOriginalPadding(); 250 mCenterEdgeItemsWhenThereAreChildren = false; 251 } 252 } 253 254 /** 255 * Returns whether the view is currently configured to center the edge children. See {@link 256 * #setEdgeItemsCenteringEnabled} for details. 257 */ 258 public boolean isEdgeItemsCenteringEnabled() { 259 return mEdgeItemsCenteringEnabled; 260 } 261} 262