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());
}
}