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