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