package carbon.widget; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.Region; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; import android.support.v4.view.ViewCompat; import android.support.v7.widget.LinearLayoutManager; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewParent; import java.util.ArrayList; import java.util.Collections; import java.util.List; import carbon.Carbon; import carbon.R; import carbon.animation.AnimatedColorStateList; import carbon.drawable.DefaultPrimaryColorStateList; import carbon.drawable.EdgeEffect; import carbon.drawable.ripple.RippleDrawable; import carbon.drawable.ripple.RippleView; import carbon.internal.ElevationComparator; import carbon.recycler.DividerItemDecoration; import carbon.shadow.ShadowView; public class RecyclerView extends android.support.v7.widget.RecyclerView implements TintedView, VisibleView { public interface OnItemClickedListener<Type> { void onItemClicked(View view, Type type, int position); } private EdgeEffect leftGlow; private EdgeEffect rightGlow; private int mTouchSlop; EdgeEffect topGlow; EdgeEffect bottomGlow; private float edgeEffectOffsetTop; private float edgeEffectOffsetBottom; private boolean drag = true; private float prevY; private int overscrollMode; private boolean clipToPadding; long prevScroll = 0; private boolean childDrawingOrderCallbackSet = false; public RecyclerView(Context context) { super(context, null, R.attr.carbon_recyclerViewStyle); initRecycler(null, R.attr.carbon_recyclerViewStyle); } public RecyclerView(Context context, AttributeSet attrs) { super(Carbon.getThemedContext(context, attrs, R.styleable.RecyclerView, R.attr.carbon_recyclerViewStyle, R.styleable.RecyclerView_carbon_theme), attrs, R.attr.carbon_recyclerViewStyle); initRecycler(attrs, R.attr.carbon_recyclerViewStyle); } public RecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { super(Carbon.getThemedContext(context, attrs, R.styleable.RecyclerView, defStyleAttr, R.styleable.RecyclerView_carbon_theme), attrs, defStyleAttr); initRecycler(attrs, defStyleAttr); } private static int[] tintIds = new int[]{ R.styleable.RecyclerView_carbon_tint, R.styleable.RecyclerView_carbon_tintMode, R.styleable.RecyclerView_carbon_backgroundTint, R.styleable.RecyclerView_carbon_backgroundTintMode, R.styleable.RecyclerView_carbon_animateColorChanges }; private void initRecycler(AttributeSet attrs, int defStyleAttr) { TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.RecyclerView, defStyleAttr, R.style.carbon_RecyclerView); for (int i = 0; i < a.getIndexCount(); i++) { int attr = a.getIndex(i); if (attr == R.styleable.RecyclerView_carbon_overScroll) { setOverScrollMode(a.getInt(attr, ViewCompat.OVER_SCROLL_ALWAYS)); } else if (attr == R.styleable.RecyclerView_carbon_headerTint) { setHeaderTint(a.getColor(attr, 0)); } else if (attr == R.styleable.RecyclerView_carbon_headerMinHeight) { setHeaderMinHeight((int) a.getDimension(attr, 0.0f)); } else if (attr == R.styleable.RecyclerView_carbon_headerParallax) { setHeaderParallax(a.getFloat(attr, 0.0f)); } else if (attr == R.styleable.RecyclerView_android_divider) { Drawable drawable = a.getDrawable(attr); float height = a.getDimension(R.styleable.RecyclerView_android_dividerHeight, 0); if (drawable != null && height > 0) { setDivider(drawable, (int) height); } } else if (attr == R.styleable.RecyclerView_edgeEffectOffsetTop) { setEdgeEffectOffsetTop(a.getDimension(attr, 0)); } else if (attr == R.styleable.RecyclerView_edgeEffectOffsetBottom) { setEdgeEffectOffsetBottom(a.getDimension(attr, 0)); } } Carbon.initTint(this, a, tintIds); a.recycle(); setClipToPadding(false); setWillNotDraw(false); } public void setDivider(Drawable divider, int height) { addItemDecoration(new DividerItemDecoration(divider, height)); } @Override public void setClipToPadding(boolean clipToPadding) { super.setClipToPadding(clipToPadding); this.clipToPadding = clipToPadding; } @Override public boolean dispatchTouchEvent(@NonNull MotionEvent ev) { if (header != null && (getChildCount() == 0 || getChildAt(0).getTop() + getScrollY() > ev.getY())) if (header.dispatchTouchEvent(ev)) return true; switch (ev.getAction()) { case MotionEvent.ACTION_MOVE: float deltaY = prevY - ev.getY(); if (!drag && Math.abs(deltaY) > mTouchSlop) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } drag = true; if (deltaY > 0) { deltaY -= mTouchSlop; } else { deltaY += mTouchSlop; } } if (drag) { final int oldY = computeVerticalScrollOffset(); int range = computeVerticalScrollRange() - getHeight(); if (header != null) range += header.getHeight(); boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS || (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); if (canOverscroll) { float pulledToY = oldY + deltaY; if (pulledToY < 0) { topGlow.onPull(deltaY / getHeight(), ev.getX() / getWidth()); if (!bottomGlow.isFinished()) bottomGlow.onRelease(); } else if (pulledToY > range) { bottomGlow.onPull(deltaY / getHeight(), 1.f - ev.getX() / getWidth()); if (!topGlow.isFinished()) topGlow.onRelease(); } if (topGlow != null && (!topGlow.isFinished() || !bottomGlow.isFinished())) postInvalidate(); } } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (drag) { drag = false; if (topGlow != null) { topGlow.onRelease(); bottomGlow.onRelease(); } } break; } prevY = ev.getY(); return super.dispatchTouchEvent(ev); } @Override public void onScrolled(int dx, int dy) { super.onScrolled(dx, dy); if (drag || topGlow == null) return; int range = computeVerticalScrollRange() - getHeight(); if (header != null) range += header.getHeight(); boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS || (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); if (canOverscroll) { long t = System.currentTimeMillis(); /*int velx = (int) (dx * 1000.0f / (t - prevScroll)); if (computeHorizontalScrollOffset() == 0 && dx < 0) { leftGlow.onAbsorb(-velx); } else if (computeHorizontalScrollOffset() == computeHorizontalScrollRange() - getWidth() && dx > 0) { rightGlow.onAbsorb(velx); }*/ int vely = (int) (dy * 1000.0f / (t - prevScroll)); if (computeVerticalScrollOffset() == 0 && dy < 0) { topGlow.onAbsorb(-vely); } else if (computeVerticalScrollOffset() == range && dy > 0) { bottomGlow.onAbsorb(vely); } prevScroll = t; } } public void setEdgeEffectOffsetTop(float edgeEffectOffsetTop) { this.edgeEffectOffsetTop = edgeEffectOffsetTop; } public void setEdgeEffectOffsetBottom(float edgeEffectOffsetBottom) { this.edgeEffectOffsetBottom = edgeEffectOffsetBottom; } @Override protected void drawableStateChanged() { super.drawableStateChanged(); updateTint(); } List<View> views = new ArrayList<>(); public List<View> getViews() { views.clear(); for (int i = 0; i < getChildCount(); i++) views.add(getChildAt(i)); return views; } private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); @Override protected void dispatchDraw(@NonNull Canvas canvas) { Collections.sort(getViews(), new ElevationComparator()); dispatchDrawWithHeader(canvas); } @Override public void setOverScrollMode(int mode) { if (mode != OVER_SCROLL_NEVER) { if (topGlow == null) { Context context = getContext(); topGlow = new EdgeEffect(context); bottomGlow = new EdgeEffect(context); updateTint(); } } else { topGlow = null; bottomGlow = null; } super.setOverScrollMode(OVER_SCROLL_NEVER); this.overscrollMode = mode; } @Override public boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) { // TODO: why isShown() returns false after being reattached? if (child instanceof ShadowView && (!Carbon.IS_LOLLIPOP || ((RenderingModeView) child).getRenderingMode() == RenderingMode.Software || ((ShadowView) child).getElevationShadowColor() != null)) { ShadowView shadowView = (ShadowView) child; shadowView.drawShadow(canvas); } if (child instanceof RippleView) { RippleView rippleView = (RippleView) child; RippleDrawable rippleDrawable = rippleView.getRippleDrawable(); if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless) { int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.translate(child.getLeft(), child.getTop()); canvas.concat(child.getMatrix()); rippleDrawable.draw(canvas); canvas.restoreToCount(saveCount); } } return super.drawChild(canvas, child, drawingTime); } @Override public boolean gatherTransparentRegion(Region region) { getViews(); return super.gatherTransparentRegion(region); } @Override protected int getChildDrawingOrder(int childCount, int child) { if (childDrawingOrderCallbackSet) return super.getChildDrawingOrder(childCount, child); return views.size() > child ? indexOfChild(views.get(child)) : child; } @Override public void setChildDrawingOrderCallback(ChildDrawingOrderCallback childDrawingOrderCallback) { super.setChildDrawingOrderCallback(childDrawingOrderCallback); childDrawingOrderCallbackSet = childDrawingOrderCallback != null; } // ------------------------------- // tint // ------------------------------- ColorStateList tint; PorterDuff.Mode tintMode; ColorStateList backgroundTint; PorterDuff.Mode backgroundTintMode; boolean animateColorChanges; ValueAnimator.AnimatorUpdateListener tintAnimatorListener = animation -> { updateTint(); ViewCompat.postInvalidateOnAnimation(RecyclerView.this); }; ValueAnimator.AnimatorUpdateListener backgroundTintAnimatorListener = animation -> { updateBackgroundTint(); ViewCompat.postInvalidateOnAnimation(RecyclerView.this); }; @Override public void setTint(ColorStateList list) { this.tint = animateColorChanges && !(list instanceof AnimatedColorStateList) ? AnimatedColorStateList.fromList(list, tintAnimatorListener) : list; updateTint(); } @Override public void setTint(int color) { if (color == 0) { setTint(new DefaultPrimaryColorStateList(getContext())); } else { setTint(ColorStateList.valueOf(color)); } } @Override public ColorStateList getTint() { return tint; } private void updateTint() { if (tint == null) return; int color = tint.getColorForState(getDrawableState(), tint.getDefaultColor()); if (leftGlow != null) leftGlow.setColor(color); if (rightGlow != null) rightGlow.setColor(color); if (topGlow != null) topGlow.setColor(color); if (bottomGlow != null) bottomGlow.setColor(color); } @Override public void setTintMode(@NonNull PorterDuff.Mode mode) { this.tintMode = mode; updateTint(); } @Override public PorterDuff.Mode getTintMode() { return tintMode; } @Override public void setBackgroundTint(ColorStateList list) { this.backgroundTint = animateColorChanges && !(list instanceof AnimatedColorStateList) ? AnimatedColorStateList.fromList(list, backgroundTintAnimatorListener) : list; updateBackgroundTint(); } @Override public void setBackgroundTint(int color) { if (color == 0) { setBackgroundTint(new DefaultPrimaryColorStateList(getContext())); } else { setBackgroundTint(ColorStateList.valueOf(color)); } } @Override public ColorStateList getBackgroundTint() { return backgroundTint; } private void updateBackgroundTint() { if (getBackground() == null) return; if (backgroundTint != null && backgroundTintMode != null) { int color = backgroundTint.getColorForState(getDrawableState(), backgroundTint.getDefaultColor()); getBackground().setColorFilter(new PorterDuffColorFilter(color, tintMode)); } else { getBackground().setColorFilter(null); } } @Override public void setBackgroundTintMode(@NonNull PorterDuff.Mode mode) { this.backgroundTintMode = mode; updateBackgroundTint(); } @Override public PorterDuff.Mode getBackgroundTintMode() { return backgroundTintMode; } public boolean isAnimateColorChangesEnabled() { return animateColorChanges; } public void setAnimateColorChangesEnabled(boolean animateColorChanges) { this.animateColorChanges = animateColorChanges; if (tint != null && !(tint instanceof AnimatedColorStateList)) setTint(AnimatedColorStateList.fromList(tint, tintAnimatorListener)); if (backgroundTint != null && !(backgroundTint instanceof AnimatedColorStateList)) setBackgroundTint(AnimatedColorStateList.fromList(backgroundTint, backgroundTintAnimatorListener)); } // ------------------------------- // scroll bars // ------------------------------- protected void onDrawHorizontalScrollBar(Canvas canvas, Drawable scrollBar, int l, int t, int r, int b) { scrollBar.setColorFilter(tint != null ? tint.getColorForState(getDrawableState(), tint.getDefaultColor()) : Color.WHITE, tintMode); scrollBar.setBounds(l, t, r, b); scrollBar.draw(canvas); } protected void onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar, int l, int t, int r, int b) { scrollBar.setColorFilter(tint != null ? tint.getColorForState(getDrawableState(), tint.getDefaultColor()) : Color.WHITE, tintMode); scrollBar.setBounds(l, t, r, b); scrollBar.draw(canvas); } // ------------------------------- // header (do not copy) // ------------------------------- View header; private float parallax = 0.5f; private int headerPadding = 0; private int headerTint = 0; private int minHeader = 0; protected void dispatchDrawWithHeader(Canvas canvas) { if (header != null) { int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG | Canvas.MATRIX_SAVE_FLAG); int headerHeight = header.getMeasuredHeight(); float scroll = computeVerticalScrollOffset(); canvas.clipRect(0, 0, getWidth(), Math.max(minHeader, headerHeight - scroll)); canvas.translate(0, -scroll * parallax); header.draw(canvas); if (headerTint != 0) { paint.setColor(headerTint); paint.setAlpha((int) (Color.alpha(headerTint) * Math.min(headerHeight - minHeader, scroll) / (headerHeight - minHeader))); canvas.drawRect(0, 0, getWidth(), Math.max(minHeader + scroll, headerHeight), paint); } canvas.restoreToCount(saveCount); saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(0, Math.max(minHeader, headerHeight - scroll), getWidth(), Integer.MAX_VALUE); super.dispatchDraw(canvas); canvas.restoreToCount(saveCount); } else { super.dispatchDraw(canvas); } if (topGlow != null) { final int scrollY = computeVerticalScrollOffset(); if (!topGlow.isFinished()) { final int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); final int width = getWidth() - getPaddingLeft() - getPaddingRight(); canvas.translate(getPaddingLeft(), edgeEffectOffsetTop + Math.min(0, scrollY)); topGlow.setSize(width, getHeight()); if (topGlow.draw(canvas)) invalidate(); canvas.restoreToCount(restoreCount); } if (!bottomGlow.isFinished()) { final int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); final int width = getWidth() - getPaddingLeft() - getPaddingRight(); final int height = getHeight(); canvas.translate(-width + getPaddingLeft(), -edgeEffectOffsetBottom + height); canvas.rotate(180, width, 0); bottomGlow.setSize(width, height); if (bottomGlow.draw(canvas)) invalidate(); canvas.restoreToCount(restoreCount); } } } public View getHeader() { return header; } public void setHeader(View view) { header = view; view.setLayoutParams(generateDefaultLayoutParams()); requestLayout(); } public void setHeader(int resId) { header = LayoutInflater.from(getContext()).inflate(resId, this, false); requestLayout(); } /** * @return parallax amount to the header applied when scrolling */ public float getHeaderParallax() { return parallax; } /** * @param amount parallax amount to apply to the header */ public void setHeaderParallax(float amount) { parallax = amount; } public int getHeaderTint() { return headerTint; } public void setHeaderTint(int color) { headerTint = color; } /** * @return min header height */ public int getHeaderMinHeight() { return minHeader; } /** * @param height min header height */ public void setHeaderMinHeight(int height) { minHeader = height; } protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int paddingTop = getPaddingTop() - headerPadding; if (header != null) { measureChildWithMargins(header, widthMeasureSpec, 0, heightMeasureSpec, 0); headerPadding = header.getMeasuredHeight(); } else { headerPadding = 0; } setPadding(getPaddingLeft(), paddingTop + headerPadding, getPaddingRight(), getPaddingBottom()); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (getLayoutManager() == null) setLayoutManager(new LinearLayoutManager(getContext())); super.onLayout(changed, l, t, r, b); if (header != null) header.layout(0, 0, getWidth(), header.getMeasuredHeight()); } }