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.Paint; 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.LayoutInflater; import android.view.MotionEvent; import android.view.View; 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 ScrollView extends android.widget.ScrollView implements TintedView, VisibleView { private int mTouchSlop; EdgeEffect topGlow; EdgeEffect bottomGlow; private boolean drag = true; private float prevY; 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 ScrollView(Context context) { super(context); initScrollView(null, android.R.attr.scrollViewStyle); } public ScrollView(Context context, AttributeSet attrs) { super(Carbon.getThemedContext(context, attrs, R.styleable.ScrollView, android.R.attr.scrollViewStyle, R.styleable.ScrollView_carbon_theme), attrs); initScrollView(attrs, android.R.attr.scrollViewStyle); } public ScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(Carbon.getThemedContext(context, attrs, R.styleable.ScrollView, defStyleAttr, R.styleable.ScrollView_carbon_theme), attrs, defStyleAttr); initScrollView(attrs, defStyleAttr); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public ScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(Carbon.getThemedContext(context, attrs, R.styleable.ScrollView, defStyleAttr, R.styleable.ScrollView_carbon_theme), attrs, defStyleAttr, defStyleRes); initScrollView(attrs, defStyleAttr); } private static int[] tintIds = new int[]{ R.styleable.ScrollView_carbon_tint, R.styleable.ScrollView_carbon_tintMode, R.styleable.ScrollView_carbon_backgroundTint, R.styleable.ScrollView_carbon_backgroundTintMode, R.styleable.ScrollView_carbon_animateColorChanges }; private void initScrollView(AttributeSet attrs, int defStyleAttr) { final ViewConfiguration configuration = ViewConfiguration.get(getContext()); mTouchSlop = configuration.getScaledTouchSlop(); TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ScrollView, defStyleAttr, R.style.carbon_ScrollView); for (int i = 0; i < a.getIndexCount(); i++) { int attr = a.getIndex(i); if (attr == R.styleable.ScrollView_carbon_overScroll) { setOverScrollMode(a.getInt(attr, OVER_SCROLL_ALWAYS)); } else if (attr == R.styleable.ScrollView_carbon_headerTint) { setHeaderTint(a.getColor(attr, 0)); } else if (attr == R.styleable.ScrollView_carbon_headerMinHeight) { setHeaderMinHeight((int) a.getDimension(attr, 0.0f)); } else if (attr == R.styleable.ScrollView_carbon_headerParallax) { setHeaderParallax(a.getFloat(attr, 0.0f)); } } Carbon.initTint(this, a, tintIds); a.recycle(); setClipToPadding(false); setWillNotDraw(false); } @Override public boolean dispatchTouchEvent(@NonNull MotionEvent ev) { if (header != null) header.dispatchTouchEvent(ev); 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(); 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 protected void onScrollChanged(int x, int y, int prevX, int prevY) { super.onScrollChanged(x, y, prevX, prevY); if (drag || topGlow == null) return; int range = computeVerticalScrollRange() - getHeight(); boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); if (canOverscroll) { long t = System.currentTimeMillis(); int dy = y - prevY; 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; } } @Override public boolean onTouchEvent(@NonNull MotionEvent ev) { try { return super.onTouchEvent(ev); } catch (IllegalArgumentException e) { // pointer index out of range, see: http://stackoverflow.com/questions/16459196/java-lang-illegalargumentexception-pointerindex-out-of-range-exception-dispat/ return true; } } @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 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(ScrollView.this); }; ValueAnimator.AnimatorUpdateListener backgroundTintAnimatorListener = animation -> { updateBackgroundTint(); ViewCompat.postInvalidateOnAnimation(ScrollView.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 (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(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; Paint paint = new Paint(); private int minHeader = 0; @Override protected void dispatchDraw(@NonNull Canvas canvas) { if (header != null) { int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG | Canvas.MATRIX_SAVE_FLAG); int headerHeight = header.getMeasuredHeight(); float scroll = getScrollY(); canvas.clipRect(0, 0, getWidth(), Math.max(minHeader + scroll, headerHeight)); 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 + scroll, headerHeight), getWidth(), Integer.MAX_VALUE); super.dispatchDraw(canvas); canvas.restoreToCount(saveCount); } else { super.dispatchDraw(canvas); } if (topGlow != null) { final int scrollY = getScrollY(); if (!topGlow.isFinished()) { final int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); final int width = getWidth() - getPaddingLeft() - getPaddingRight(); canvas.translate(getPaddingLeft(), 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(), Math.max(computeVerticalScrollRange() - getHeight(), scrollY) + 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; requestLayout(); } public void setHeader(int resId) { header = LayoutInflater.from(getContext()).inflate(resId, this, false); requestLayout(); } public float getHeaderParallax() { return parallax; } public void setHeaderParallax(float amount) { parallax = amount; } public int getHeaderTint() { return headerTint; } public void setHeaderTint(int color) { headerTint = color; } public int getHeaderMinHeight() { return minHeader; } 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) { super.onLayout(changed, l, t, r, b); if (header != null) header.layout(0, 0, getWidth(), header.getMeasuredHeight()); } }