package carbon.widget; import android.animation.ValueAnimator; import android.annotation.TargetApi; 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.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.NonNull; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.view.ViewParent; import carbon.Carbon; import carbon.R; import carbon.animation.AnimatedColorStateList; import carbon.drawable.DefaultPrimaryColorStateList; import carbon.drawable.EdgeEffect; public class HorizontalScrollView extends android.widget.HorizontalScrollView implements TintedView, VisibleView { private int mTouchSlop; EdgeEffect leftGlow; EdgeEffect rightGlow; private boolean drag = true; private float prevX; private int overscrollMode; long prevScroll = 0; public static final int OVER_SCROLL_ALWAYS = 0; public static final int OVER_SCROLL_IF_CONTENT_SCROLLS = 1; public static final int OVER_SCROLL_NEVER = 2; public HorizontalScrollView(Context context) { super(context); initHorizontalScrollView(null, android.R.attr.horizontalScrollViewStyle); } public HorizontalScrollView(Context context, AttributeSet attrs) { super(Carbon.getThemedContext(context, attrs, R.styleable.HorizontalScrollView, android.R.attr.horizontalScrollViewStyle, R.styleable.HorizontalScrollView_carbon_theme), attrs); initHorizontalScrollView(attrs, android.R.attr.horizontalScrollViewStyle); } public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(Carbon.getThemedContext(context, attrs, R.styleable.HorizontalScrollView, defStyleAttr, R.styleable.HorizontalScrollView_carbon_theme), attrs, defStyleAttr); initHorizontalScrollView(attrs, defStyleAttr); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(Carbon.getThemedContext(context, attrs, R.styleable.HorizontalScrollView, defStyleAttr, R.styleable.HorizontalScrollView_carbon_theme), attrs, defStyleAttr, defStyleRes); initHorizontalScrollView(attrs, defStyleAttr); } private static int[] tintIds = new int[]{ R.styleable.HorizontalScrollView_carbon_tint, R.styleable.HorizontalScrollView_carbon_tintMode, R.styleable.HorizontalScrollView_carbon_backgroundTint, R.styleable.HorizontalScrollView_carbon_backgroundTintMode, R.styleable.HorizontalScrollView_carbon_animateColorChanges }; private void initHorizontalScrollView(AttributeSet attrs, int defStyleAttr) { final ViewConfiguration configuration = ViewConfiguration.get(getContext()); mTouchSlop = configuration.getScaledTouchSlop(); TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.HorizontalScrollView, defStyleAttr, R.style.carbon_HorizontalScrollView); for (int i = 0; i < a.getIndexCount(); i++) { int attr = a.getIndex(i); if (attr == R.styleable.HorizontalScrollView_carbon_overScroll) { setOverScrollMode(a.getInt(attr, OVER_SCROLL_ALWAYS)); } } Carbon.initTint(this, a, tintIds); a.recycle(); setWillNotDraw(false); } @Override public void draw(@NonNull Canvas canvas) { super.draw(canvas); if (leftGlow != null) { final int scrollX = getScrollX(); if (!leftGlow.isFinished()) { final int restoreCount = canvas.save(); final int height = getHeight() - getPaddingTop() - getPaddingBottom(); canvas.rotate(270); canvas.translate(-height + getPaddingTop(), Math.min(0, scrollX)); leftGlow.setSize(height, getWidth()); if (leftGlow.draw(canvas)) { postInvalidate(); } canvas.restoreToCount(restoreCount); } if (!rightGlow.isFinished()) { final int restoreCount = canvas.save(); final int width = getWidth(); final int height = getHeight() - getPaddingTop() - getPaddingBottom(); canvas.rotate(90); canvas.translate(-getPaddingTop(), -(Math.max(computeHorizontalScrollRange() - getWidth(), scrollX) + width)); rightGlow.setSize(height, width); if (rightGlow.draw(canvas)) { postInvalidate(); } canvas.restoreToCount(restoreCount); } } } @Override public boolean dispatchTouchEvent(@NonNull MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_MOVE: float deltaX = prevX - ev.getX(); if (!drag && Math.abs(deltaX) > mTouchSlop) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } drag = true; if (deltaX > 0) { deltaX -= mTouchSlop; } else { deltaX += mTouchSlop; } } if (drag) { final int oldX = getScrollX(); final int range = computeHorizontalScrollRange() - getWidth(); boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); if (canOverscroll) { float pulledToY = oldX + deltaX; if (pulledToY < 0) { leftGlow.onPull(deltaX / getWidth(), 1.f - ev.getY() / getHeight()); if (!rightGlow.isFinished()) rightGlow.onRelease(); } else if (pulledToY > range) { rightGlow.onPull(deltaX / getWidth(), ev.getY() / getHeight()); if (!leftGlow.isFinished()) leftGlow.onRelease(); } if (leftGlow != null && (!leftGlow.isFinished() || !rightGlow.isFinished())) postInvalidate(); } } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (drag) { drag = false; if (leftGlow != null) { leftGlow.onRelease(); rightGlow.onRelease(); } } break; } prevX = ev.getX(); return super.dispatchTouchEvent(ev); } @Override protected void onScrollChanged(int x, int y, int prevX, int prevY) { super.onScrollChanged(x, y, prevX, prevY); if (drag || leftGlow == null) return; final int range = computeHorizontalScrollRange() - getWidth(); boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); if (canOverscroll) { int dx = x - prevX; long t = System.currentTimeMillis(); int velx = (int) (dx * 1000.0f / (t - prevScroll)); if (computeHorizontalScrollOffset() == 0 && dx < 0) { leftGlow.onAbsorb(-velx); } else if (computeHorizontalScrollOffset() == range && dx > 0) { rightGlow.onAbsorb(velx); } prevScroll = t; } } @Override public void setOverScrollMode(int mode) { if (mode != OVER_SCROLL_NEVER) { if (leftGlow == null) { Context context = getContext(); leftGlow = new EdgeEffect(context); rightGlow = new EdgeEffect(context); updateTint(); } } else { leftGlow = null; rightGlow = null; } super.setOverScrollMode(OVER_SCROLL_NEVER); this.overscrollMode = mode; } @Override protected void drawableStateChanged() { super.drawableStateChanged(); updateTint(); } // ------------------------------- // tint // ------------------------------- ColorStateList tint; PorterDuff.Mode tintMode; ColorStateList backgroundTint; PorterDuff.Mode backgroundTintMode; boolean animateColorChanges; ValueAnimator.AnimatorUpdateListener tintAnimatorListener = animation -> { updateTint(); ViewCompat.postInvalidateOnAnimation(HorizontalScrollView.this); }; ValueAnimator.AnimatorUpdateListener backgroundTintAnimatorListener = animation -> { updateBackgroundTint(); ViewCompat.postInvalidateOnAnimation(HorizontalScrollView.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); } @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); } }