HorizontalGridView.java revision 3e91a20766ee7aea2bd2a8282425b0d61cd44376
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14package android.support.v17.leanback.widget; 15 16import android.content.Context; 17import android.content.res.TypedArray; 18import android.graphics.Bitmap; 19import android.graphics.Canvas; 20import android.graphics.Color; 21import android.graphics.LinearGradient; 22import android.graphics.Paint; 23import android.graphics.PorterDuff; 24import android.graphics.PorterDuffXfermode; 25import android.graphics.Rect; 26import android.graphics.Shader; 27import android.support.v17.leanback.R; 28import android.support.v7.widget.RecyclerView; 29import android.util.AttributeSet; 30import android.util.TypedValue; 31import android.view.View; 32 33/** 34 * A view that shows items in a horizontal scrolling list. The items come from 35 * the {@link RecyclerView.Adapter} associated with this view. 36 */ 37public class HorizontalGridView extends BaseGridView { 38 39 private boolean mFadingLowEdge; 40 private boolean mFadingHighEdge; 41 42 private Paint mTempPaint = new Paint(); 43 private Bitmap mTempBitmapLow; 44 private LinearGradient mLowFadeShader; 45 private int mLowFadeShaderLength; 46 private int mLowFadeShaderOffset; 47 private Bitmap mTempBitmapHigh; 48 private LinearGradient mHighFadeShader; 49 private int mHighFadeShaderLength; 50 private int mHighFadeShaderOffset; 51 private Rect mTempRect = new Rect(); 52 53 public HorizontalGridView(Context context) { 54 this(context, null); 55 } 56 57 public HorizontalGridView(Context context, AttributeSet attrs) { 58 this(context, attrs, 0); 59 } 60 61 public HorizontalGridView(Context context, AttributeSet attrs, int defStyle) { 62 super(context, attrs, defStyle); 63 mLayoutManager.setOrientation(RecyclerView.HORIZONTAL); 64 initAttributes(context, attrs); 65 } 66 67 protected void initAttributes(Context context, AttributeSet attrs) { 68 initBaseGridViewAttributes(context, attrs); 69 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbHorizontalGridView); 70 setRowHeight(a); 71 setNumRows(a.getInt(R.styleable.lbHorizontalGridView_numberOfRows, 1)); 72 a.recycle(); 73 updateLayerType(); 74 mTempPaint = new Paint(); 75 mTempPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); 76 } 77 78 void setRowHeight(TypedArray array) { 79 TypedValue typedValue = array.peekValue(R.styleable.lbHorizontalGridView_rowHeight); 80 int size; 81 if (typedValue != null && typedValue.type == TypedValue.TYPE_DIMENSION) { 82 size = array.getDimensionPixelSize(R.styleable.lbHorizontalGridView_rowHeight, 0); 83 } else { 84 size = array.getInt(R.styleable.lbHorizontalGridView_rowHeight, 0); 85 } 86 setRowHeight(size); 87 } 88 89 /** 90 * Set the number of rows. Defaults to one. 91 */ 92 public void setNumRows(int numRows) { 93 mLayoutManager.setNumRows(numRows); 94 requestLayout(); 95 } 96 97 /** 98 * Set the row height. 99 * 100 * @param height May be WRAP_CONTENT, or a size in pixels. If zero, 101 * row height will be fixed based on number of rows and view height. 102 */ 103 public void setRowHeight(int height) { 104 mLayoutManager.setRowHeight(height); 105 requestLayout(); 106 } 107 108 /** 109 * Set fade out left edge to transparent. Note turn on fading edge is very expensive 110 * that you should turn off when HorizontalGridView is scrolling. 111 */ 112 public final void setFadingLeftEdge(boolean fading) { 113 if (mFadingLowEdge != fading) { 114 mFadingLowEdge = fading; 115 if (!mFadingLowEdge) { 116 mTempBitmapLow = null; 117 } 118 invalidate(); 119 updateLayerType(); 120 } 121 } 122 123 /** 124 * Return true if fading left edge. 125 */ 126 public final boolean getFadingLeftEdge() { 127 return mFadingLowEdge; 128 } 129 130 /** 131 * Set left edge fading length in pixels. 132 */ 133 public final void setFadingLeftEdgeLength(int fadeLength) { 134 if (mLowFadeShaderLength != fadeLength) { 135 mLowFadeShaderLength = fadeLength; 136 if (mLowFadeShaderLength != 0) { 137 mLowFadeShader = new LinearGradient(0, 0, mLowFadeShaderLength, 0, 138 Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP); 139 } else { 140 mLowFadeShader = null; 141 } 142 invalidate(); 143 } 144 } 145 146 /** 147 * Get left edge fading length in pixels. 148 */ 149 public final int getFadingLeftEdgeLength() { 150 return mLowFadeShaderLength; 151 } 152 153 /** 154 * Set distance in pixels between fading start position and left padding edge. 155 * The fading start position is positive when start position is inside left padding 156 * area. Default value is 0, means that the fading starts from left padding edge. 157 */ 158 public final void setFadingLeftEdgeOffset(int fadeOffset) { 159 if (mLowFadeShaderOffset != fadeOffset) { 160 mLowFadeShaderOffset = fadeOffset; 161 invalidate(); 162 } 163 } 164 165 /** 166 * Get distance in pixels between fading start position and left padding edge. 167 * The fading start position is positive when start position is inside left padding 168 * area. Default value is 0, means that the fading starts from left padding edge. 169 */ 170 public final int getFadingLeftEdgeOffset() { 171 return mLowFadeShaderOffset; 172 } 173 174 /** 175 * Set fade out right edge to transparent. Note turn on fading edge is very expensive 176 * that you should turn off when HorizontalGridView is scrolling. 177 */ 178 public final void setFadingRightEdge(boolean fading) { 179 if (mFadingHighEdge != fading) { 180 mFadingHighEdge = fading; 181 if (!mFadingHighEdge) { 182 mTempBitmapHigh = null; 183 } 184 invalidate(); 185 updateLayerType(); 186 } 187 } 188 189 /** 190 * Return true if fading right edge. 191 */ 192 public final boolean getFadingRightEdge() { 193 return mFadingHighEdge; 194 } 195 196 /** 197 * Set right edge fading length in pixels. 198 */ 199 public final void setFadingRightEdgeLength(int fadeLength) { 200 if (mHighFadeShaderLength != fadeLength) { 201 mHighFadeShaderLength = fadeLength; 202 if (mHighFadeShaderLength != 0) { 203 mHighFadeShader = new LinearGradient(0, 0, mHighFadeShaderLength, 0, 204 Color.BLACK, Color.TRANSPARENT, Shader.TileMode.CLAMP); 205 } else { 206 mHighFadeShader = null; 207 } 208 invalidate(); 209 } 210 } 211 212 /** 213 * Get right edge fading length in pixels. 214 */ 215 public final int getFadingRightEdgeLength() { 216 return mHighFadeShaderLength; 217 } 218 219 /** 220 * Get distance in pixels between fading start position and right padding edge. 221 * The fading start position is positive when start position is inside right padding 222 * area. Default value is 0, means that the fading starts from right padding edge. 223 */ 224 public final void setFadingRightEdgeOffset(int fadeOffset) { 225 if (mHighFadeShaderOffset != fadeOffset) { 226 mHighFadeShaderOffset = fadeOffset; 227 invalidate(); 228 } 229 } 230 231 /** 232 * Set distance in pixels between fading start position and right padding edge. 233 * The fading start position is positive when start position is inside right padding 234 * area. Default value is 0, means that the fading starts from right padding edge. 235 */ 236 public final int getFadingRightEdgeOffset() { 237 return mHighFadeShaderOffset; 238 } 239 240 private boolean needsFadingLowEdge() { 241 if (!mFadingLowEdge) { 242 return false; 243 } 244 final int c = getChildCount(); 245 for (int i = 0; i < c; i++) { 246 View view = getChildAt(i); 247 if (mLayoutManager.getOpticalLeft(view) < 248 getPaddingLeft() - mLowFadeShaderOffset) { 249 return true; 250 } 251 } 252 return false; 253 } 254 255 private boolean needsFadingHighEdge() { 256 if (!mFadingHighEdge) { 257 return false; 258 } 259 final int c = getChildCount(); 260 for (int i = c - 1; i >= 0; i--) { 261 View view = getChildAt(i); 262 if (mLayoutManager.getOpticalRight(view) > getWidth() 263 - getPaddingRight() + mHighFadeShaderOffset) { 264 return true; 265 } 266 } 267 return false; 268 } 269 270 private Bitmap getTempBitmapLow() { 271 if (mTempBitmapLow == null 272 || mTempBitmapLow.getWidth() != mLowFadeShaderLength 273 || mTempBitmapLow.getHeight() != getHeight()) { 274 mTempBitmapLow = Bitmap.createBitmap(mLowFadeShaderLength, getHeight(), 275 Bitmap.Config.ARGB_8888); 276 } 277 return mTempBitmapLow; 278 } 279 280 private Bitmap getTempBitmapHigh() { 281 if (mTempBitmapHigh == null 282 || mTempBitmapHigh.getWidth() != mHighFadeShaderLength 283 || mTempBitmapHigh.getHeight() != getHeight()) { 284 // TODO: fix logic for sharing mTempBitmapLow 285 if (false && mTempBitmapLow != null 286 && mTempBitmapLow.getWidth() == mHighFadeShaderLength 287 && mTempBitmapLow.getHeight() == getHeight()) { 288 // share same bitmap for low edge fading and high edge fading. 289 mTempBitmapHigh = mTempBitmapLow; 290 } else { 291 mTempBitmapHigh = Bitmap.createBitmap(mHighFadeShaderLength, getHeight(), 292 Bitmap.Config.ARGB_8888); 293 } 294 } 295 return mTempBitmapHigh; 296 } 297 298 @Override 299 public void draw(Canvas canvas) { 300 final boolean needsFadingLow = needsFadingLowEdge(); 301 final boolean needsFadingHigh = needsFadingHighEdge(); 302 if (!needsFadingLow) { 303 mTempBitmapLow = null; 304 } 305 if (!needsFadingHigh) { 306 mTempBitmapHigh = null; 307 } 308 if (!needsFadingLow && !needsFadingHigh) { 309 super.draw(canvas); 310 return; 311 } 312 313 int lowEdge = mFadingLowEdge? getPaddingLeft() - mLowFadeShaderOffset - mLowFadeShaderLength : 0; 314 int highEdge = mFadingHighEdge ? getWidth() - getPaddingRight() 315 + mHighFadeShaderOffset + mHighFadeShaderLength : getWidth(); 316 317 // draw not-fade content 318 int save = canvas.save(); 319 canvas.clipRect(lowEdge + (mFadingLowEdge ? mLowFadeShaderLength : 0), 0, 320 highEdge - (mFadingHighEdge ? mHighFadeShaderLength : 0), getHeight()); 321 super.draw(canvas); 322 canvas.restoreToCount(save); 323 324 Canvas tmpCanvas = new Canvas(); 325 mTempRect.top = 0; 326 mTempRect.bottom = getHeight(); 327 if (needsFadingLow && mLowFadeShaderLength > 0) { 328 Bitmap tempBitmap = getTempBitmapLow(); 329 tempBitmap.eraseColor(Color.TRANSPARENT); 330 tmpCanvas.setBitmap(tempBitmap); 331 // draw original content 332 int tmpSave = tmpCanvas.save(); 333 tmpCanvas.clipRect(0, 0, mLowFadeShaderLength, getHeight()); 334 tmpCanvas.translate(-lowEdge, 0); 335 super.draw(tmpCanvas); 336 tmpCanvas.restoreToCount(tmpSave); 337 // draw fading out 338 mTempPaint.setShader(mLowFadeShader); 339 tmpCanvas.drawRect(0, 0, mLowFadeShaderLength, getHeight(), mTempPaint); 340 // copy back to canvas 341 mTempRect.left = 0; 342 mTempRect.right = mLowFadeShaderLength; 343 canvas.translate(lowEdge, 0); 344 canvas.drawBitmap(tempBitmap, mTempRect, mTempRect, null); 345 canvas.translate(-lowEdge, 0); 346 } 347 if (needsFadingHigh && mHighFadeShaderLength > 0) { 348 Bitmap tempBitmap = getTempBitmapHigh(); 349 tempBitmap.eraseColor(Color.TRANSPARENT); 350 tmpCanvas.setBitmap(tempBitmap); 351 // draw original content 352 int tmpSave = tmpCanvas.save(); 353 tmpCanvas.clipRect(0, 0, mHighFadeShaderLength, getHeight()); 354 tmpCanvas.translate(-(highEdge - mHighFadeShaderLength), 0); 355 super.draw(tmpCanvas); 356 tmpCanvas.restoreToCount(tmpSave); 357 // draw fading out 358 mTempPaint.setShader(mHighFadeShader); 359 tmpCanvas.drawRect(0, 0, mHighFadeShaderLength, getHeight(), mTempPaint); 360 // copy back to canvas 361 mTempRect.left = 0; 362 mTempRect.right = mHighFadeShaderLength; 363 canvas.translate(highEdge - mHighFadeShaderLength, 0); 364 canvas.drawBitmap(tempBitmap, mTempRect, mTempRect, null); 365 canvas.translate(-(highEdge - mHighFadeShaderLength), 0); 366 } 367 } 368 369 /** 370 * Updates the layer type for this view. 371 * If fading edges are needed, use a hardware layer. This works around the problem 372 * that when a child invalidates itself (for example has an animated background), 373 * the parent view must also be invalidated to refresh the display list which 374 * updates the the caching bitmaps used to draw the fading edges. 375 */ 376 private void updateLayerType() { 377 if (mFadingLowEdge || mFadingHighEdge) { 378 setLayerType(View.LAYER_TYPE_HARDWARE, null); 379 setWillNotDraw(false); 380 } else { 381 setLayerType(View.LAYER_TYPE_NONE, null); 382 setWillNotDraw(true); 383 } 384 } 385} 386