HorizontalGridView.java revision a00bada00bff4a58436a39472ab14ccb7a8f619d
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 {@link android.view.ViewGroup} 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 if (typedValue != null) { 81 int size = array.getLayoutDimension(R.styleable.lbHorizontalGridView_rowHeight, 0); 82 setRowHeight(size); 83 } 84 } 85 86 /** 87 * Sets the number of rows. Defaults to one. 88 */ 89 public void setNumRows(int numRows) { 90 mLayoutManager.setNumRows(numRows); 91 requestLayout(); 92 } 93 94 /** 95 * Sets the row height. 96 * 97 * @param height May be {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT WRAP_CONTENT}, 98 * or a size in pixels. If zero, row height will be fixed based on number of 99 * rows and view height. 100 */ 101 public void setRowHeight(int height) { 102 mLayoutManager.setRowHeight(height); 103 requestLayout(); 104 } 105 106 /** 107 * Sets the fade out left edge to transparent. Note turn on fading edge is very expensive 108 * that you should turn off when HorizontalGridView is scrolling. 109 */ 110 public final void setFadingLeftEdge(boolean fading) { 111 if (mFadingLowEdge != fading) { 112 mFadingLowEdge = fading; 113 if (!mFadingLowEdge) { 114 mTempBitmapLow = null; 115 } 116 invalidate(); 117 updateLayerType(); 118 } 119 } 120 121 /** 122 * Returns true if left edge fading is enabled. 123 */ 124 public final boolean getFadingLeftEdge() { 125 return mFadingLowEdge; 126 } 127 128 /** 129 * Sets the left edge fading length in pixels. 130 */ 131 public final void setFadingLeftEdgeLength(int fadeLength) { 132 if (mLowFadeShaderLength != fadeLength) { 133 mLowFadeShaderLength = fadeLength; 134 if (mLowFadeShaderLength != 0) { 135 mLowFadeShader = new LinearGradient(0, 0, mLowFadeShaderLength, 0, 136 Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP); 137 } else { 138 mLowFadeShader = null; 139 } 140 invalidate(); 141 } 142 } 143 144 /** 145 * Returns the left edge fading length in pixels. 146 */ 147 public final int getFadingLeftEdgeLength() { 148 return mLowFadeShaderLength; 149 } 150 151 /** 152 * Sets the distance in pixels between fading start position and left padding edge. 153 * The fading start position is positive when start position is inside left padding 154 * area. Default value is 0, means that the fading starts from left padding edge. 155 */ 156 public final void setFadingLeftEdgeOffset(int fadeOffset) { 157 if (mLowFadeShaderOffset != fadeOffset) { 158 mLowFadeShaderOffset = fadeOffset; 159 invalidate(); 160 } 161 } 162 163 /** 164 * Returns the distance in pixels between fading start position and left padding edge. 165 * The fading start position is positive when start position is inside left padding 166 * area. Default value is 0, means that the fading starts from left padding edge. 167 */ 168 public final int getFadingLeftEdgeOffset() { 169 return mLowFadeShaderOffset; 170 } 171 172 /** 173 * Sets the fade out right edge to transparent. Note turn on fading edge is very expensive 174 * that you should turn off when HorizontalGridView is scrolling. 175 */ 176 public final void setFadingRightEdge(boolean fading) { 177 if (mFadingHighEdge != fading) { 178 mFadingHighEdge = fading; 179 if (!mFadingHighEdge) { 180 mTempBitmapHigh = null; 181 } 182 invalidate(); 183 updateLayerType(); 184 } 185 } 186 187 /** 188 * Returns true if fading right edge is enabled. 189 */ 190 public final boolean getFadingRightEdge() { 191 return mFadingHighEdge; 192 } 193 194 /** 195 * Sets the right edge fading length in pixels. 196 */ 197 public final void setFadingRightEdgeLength(int fadeLength) { 198 if (mHighFadeShaderLength != fadeLength) { 199 mHighFadeShaderLength = fadeLength; 200 if (mHighFadeShaderLength != 0) { 201 mHighFadeShader = new LinearGradient(0, 0, mHighFadeShaderLength, 0, 202 Color.BLACK, Color.TRANSPARENT, Shader.TileMode.CLAMP); 203 } else { 204 mHighFadeShader = null; 205 } 206 invalidate(); 207 } 208 } 209 210 /** 211 * Returns the right edge fading length in pixels. 212 */ 213 public final int getFadingRightEdgeLength() { 214 return mHighFadeShaderLength; 215 } 216 217 /** 218 * Returns the distance in pixels between fading start position and right padding edge. 219 * The fading start position is positive when start position is inside right padding 220 * area. Default value is 0, means that the fading starts from right padding edge. 221 */ 222 public final void setFadingRightEdgeOffset(int fadeOffset) { 223 if (mHighFadeShaderOffset != fadeOffset) { 224 mHighFadeShaderOffset = fadeOffset; 225 invalidate(); 226 } 227 } 228 229 /** 230 * Sets the distance in pixels between fading start position and right padding edge. 231 * The fading start position is positive when start position is inside right padding 232 * area. Default value is 0, means that the fading starts from right padding edge. 233 */ 234 public final int getFadingRightEdgeOffset() { 235 return mHighFadeShaderOffset; 236 } 237 238 private boolean needsFadingLowEdge() { 239 if (!mFadingLowEdge) { 240 return false; 241 } 242 final int c = getChildCount(); 243 for (int i = 0; i < c; i++) { 244 View view = getChildAt(i); 245 if (mLayoutManager.getOpticalLeft(view) < 246 getPaddingLeft() - mLowFadeShaderOffset) { 247 return true; 248 } 249 } 250 return false; 251 } 252 253 private boolean needsFadingHighEdge() { 254 if (!mFadingHighEdge) { 255 return false; 256 } 257 final int c = getChildCount(); 258 for (int i = c - 1; i >= 0; i--) { 259 View view = getChildAt(i); 260 if (mLayoutManager.getOpticalRight(view) > getWidth() 261 - getPaddingRight() + mHighFadeShaderOffset) { 262 return true; 263 } 264 } 265 return false; 266 } 267 268 private Bitmap getTempBitmapLow() { 269 if (mTempBitmapLow == null 270 || mTempBitmapLow.getWidth() != mLowFadeShaderLength 271 || mTempBitmapLow.getHeight() != getHeight()) { 272 mTempBitmapLow = Bitmap.createBitmap(mLowFadeShaderLength, getHeight(), 273 Bitmap.Config.ARGB_8888); 274 } 275 return mTempBitmapLow; 276 } 277 278 private Bitmap getTempBitmapHigh() { 279 if (mTempBitmapHigh == null 280 || mTempBitmapHigh.getWidth() != mHighFadeShaderLength 281 || mTempBitmapHigh.getHeight() != getHeight()) { 282 // TODO: fix logic for sharing mTempBitmapLow 283 if (false && mTempBitmapLow != null 284 && mTempBitmapLow.getWidth() == mHighFadeShaderLength 285 && mTempBitmapLow.getHeight() == getHeight()) { 286 // share same bitmap for low edge fading and high edge fading. 287 mTempBitmapHigh = mTempBitmapLow; 288 } else { 289 mTempBitmapHigh = Bitmap.createBitmap(mHighFadeShaderLength, getHeight(), 290 Bitmap.Config.ARGB_8888); 291 } 292 } 293 return mTempBitmapHigh; 294 } 295 296 @Override 297 public void draw(Canvas canvas) { 298 final boolean needsFadingLow = needsFadingLowEdge(); 299 final boolean needsFadingHigh = needsFadingHighEdge(); 300 if (!needsFadingLow) { 301 mTempBitmapLow = null; 302 } 303 if (!needsFadingHigh) { 304 mTempBitmapHigh = null; 305 } 306 if (!needsFadingLow && !needsFadingHigh) { 307 super.draw(canvas); 308 return; 309 } 310 311 int lowEdge = mFadingLowEdge? getPaddingLeft() - mLowFadeShaderOffset - mLowFadeShaderLength : 0; 312 int highEdge = mFadingHighEdge ? getWidth() - getPaddingRight() 313 + mHighFadeShaderOffset + mHighFadeShaderLength : getWidth(); 314 315 // draw not-fade content 316 int save = canvas.save(); 317 canvas.clipRect(lowEdge + (mFadingLowEdge ? mLowFadeShaderLength : 0), 0, 318 highEdge - (mFadingHighEdge ? mHighFadeShaderLength : 0), getHeight()); 319 super.draw(canvas); 320 canvas.restoreToCount(save); 321 322 Canvas tmpCanvas = new Canvas(); 323 mTempRect.top = 0; 324 mTempRect.bottom = getHeight(); 325 if (needsFadingLow && mLowFadeShaderLength > 0) { 326 Bitmap tempBitmap = getTempBitmapLow(); 327 tempBitmap.eraseColor(Color.TRANSPARENT); 328 tmpCanvas.setBitmap(tempBitmap); 329 // draw original content 330 int tmpSave = tmpCanvas.save(); 331 tmpCanvas.clipRect(0, 0, mLowFadeShaderLength, getHeight()); 332 tmpCanvas.translate(-lowEdge, 0); 333 super.draw(tmpCanvas); 334 tmpCanvas.restoreToCount(tmpSave); 335 // draw fading out 336 mTempPaint.setShader(mLowFadeShader); 337 tmpCanvas.drawRect(0, 0, mLowFadeShaderLength, getHeight(), mTempPaint); 338 // copy back to canvas 339 mTempRect.left = 0; 340 mTempRect.right = mLowFadeShaderLength; 341 canvas.translate(lowEdge, 0); 342 canvas.drawBitmap(tempBitmap, mTempRect, mTempRect, null); 343 canvas.translate(-lowEdge, 0); 344 } 345 if (needsFadingHigh && mHighFadeShaderLength > 0) { 346 Bitmap tempBitmap = getTempBitmapHigh(); 347 tempBitmap.eraseColor(Color.TRANSPARENT); 348 tmpCanvas.setBitmap(tempBitmap); 349 // draw original content 350 int tmpSave = tmpCanvas.save(); 351 tmpCanvas.clipRect(0, 0, mHighFadeShaderLength, getHeight()); 352 tmpCanvas.translate(-(highEdge - mHighFadeShaderLength), 0); 353 super.draw(tmpCanvas); 354 tmpCanvas.restoreToCount(tmpSave); 355 // draw fading out 356 mTempPaint.setShader(mHighFadeShader); 357 tmpCanvas.drawRect(0, 0, mHighFadeShaderLength, getHeight(), mTempPaint); 358 // copy back to canvas 359 mTempRect.left = 0; 360 mTempRect.right = mHighFadeShaderLength; 361 canvas.translate(highEdge - mHighFadeShaderLength, 0); 362 canvas.drawBitmap(tempBitmap, mTempRect, mTempRect, null); 363 canvas.translate(-(highEdge - mHighFadeShaderLength), 0); 364 } 365 } 366 367 /** 368 * Updates the layer type for this view. 369 * If fading edges are needed, use a hardware layer. This works around the problem 370 * that when a child invalidates itself (for example has an animated background), 371 * the parent view must also be invalidated to refresh the display list which 372 * updates the the caching bitmaps used to draw the fading edges. 373 */ 374 private void updateLayerType() { 375 if (mFadingLowEdge || mFadingHighEdge) { 376 setLayerType(View.LAYER_TYPE_HARDWARE, null); 377 setWillNotDraw(false); 378 } else { 379 setLayerType(View.LAYER_TYPE_NONE, null); 380 setWillNotDraw(true); 381 } 382 } 383} 384