/*
* Copyright (C) 2015 Simon Vig Therkildsen
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.simonvt.cathode.widget;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.WindowInsetsCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.support.v7.widget.Toolbar;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import net.simonvt.cathode.R;
import net.simonvt.cathode.widget.ObservableScrollView.ScrollListener;
public class AppBarRelativeLayout extends RelativeLayout {
private static final int SCRIM_ANIMATION_DURATION = 600;
static final Interpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator();
private int contentTopViewId;
private CollapsingTextHelper textHelper;
private Toolbar toolbar;
private View dummyView;
private View contentView;
private WindowInsetsCompat lastInsets;
private int insetsTop;
private int offset;
private int expandedMarginLeft;
private int expandedMarginTop;
private int expandedMarginBottom;
private int expandedMarginRight;
private Drawable contentScrim;
private Drawable statusBarScrim;
private Rect dummyBounds = new Rect();
private Rect collapsedBounds = new Rect();
private Rect expandedBounds = new Rect();
private boolean scrimsVisible;
private ValueAnimator scrimAnimator;
private int scrimAlpha;
public AppBarRelativeLayout(Context context) {
this(context, null);
}
public AppBarRelativeLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AppBarRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setWillNotDraw(false);
textHelper = new CollapsingTextHelper(this);
textHelper.setExpandedTextGravity(Gravity.LEFT | Gravity.BOTTOM);
textHelper.setTextSizeInterpolator(new DecelerateInterpolator());
TypedArray a =
context.obtainStyledAttributes(attrs, R.styleable.AppBarRelativeLayout, defStyleAttr,
R.style.AppBarRelativeLayout);
contentTopViewId =
a.getResourceId(R.styleable.AppBarRelativeLayout_contentTopViewId, R.id.appBarContent);
expandedMarginLeft = expandedMarginTop = expandedMarginRight = expandedMarginBottom =
a.getDimensionPixelSize(R.styleable.AppBarRelativeLayout_expandedTitleMargin, 0);
if (a.hasValue(R.styleable.AppBarRelativeLayout_expandedTitleMarginStart)) {
expandedMarginLeft =
a.getDimensionPixelSize(R.styleable.AppBarRelativeLayout_expandedTitleMarginStart, 0);
}
if (a.hasValue(R.styleable.AppBarRelativeLayout_expandedTitleMarginEnd)) {
expandedMarginRight =
a.getDimensionPixelSize(R.styleable.AppBarRelativeLayout_expandedTitleMarginEnd, 0);
}
if (a.hasValue(R.styleable.AppBarRelativeLayout_expandedTitleMarginTop)) {
expandedMarginTop =
a.getDimensionPixelSize(R.styleable.AppBarRelativeLayout_expandedTitleMarginTop, 0);
}
if (a.hasValue(R.styleable.AppBarRelativeLayout_expandedTitleMarginBottom)) {
expandedMarginBottom =
a.getDimensionPixelSize(R.styleable.AppBarRelativeLayout_expandedTitleMarginBottom, 0);
}
final int collapsedTextAppearance =
a.getResourceId(R.styleable.AppBarRelativeLayout_collapsedTitleTextAppearance,
R.style.TextAppearance_AppCompat_Widget_ActionBar_Title);
textHelper.setCollapsedTextAppearance(collapsedTextAppearance);
final int expandedTextAppearance =
a.getResourceId(R.styleable.AppBarRelativeLayout_expandedTitleTextAppearance,
R.style.TextAppearance_AppCompat_Title);
textHelper.setExpandedTextAppearance(expandedTextAppearance);
contentScrim = a.getDrawable(R.styleable.AppBarRelativeLayout_contentScrim);
if (contentScrim != null) {
contentScrim.setCallback(this);
}
statusBarScrim = a.getDrawable(R.styleable.AppBarRelativeLayout_statusBarScrim);
if (statusBarScrim != null) {
statusBarScrim.setCallback(this);
}
a.recycle();
ViewCompat.setOnApplyWindowInsetsListener(this,
new android.support.v4.view.OnApplyWindowInsetsListener() {
@Override
public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
lastInsets = insets;
insetsTop = lastInsets.getSystemWindowInsetTop();
return insets.consumeSystemWindowInsets();
}
});
}
public void setTitle(CharSequence title) {
textHelper.setText(title);
}
@Override protected void onFinishInflate() {
super.onFinishInflate();
toolbar = (Toolbar) findViewById(R.id.toolbar);
contentView = findViewById(contentTopViewId);
}
@Override protected void onAttachedToWindow() {
super.onAttachedToWindow();
ObservableScrollView parent = (ObservableScrollView) getParent();
parent.addListener(scrollListener);
}
private ScrollListener scrollListener = new ScrollListener() {
@Override public void onScrollChanged(int l, int t) {
if (t == offset) {
return;
}
int offsetBy = t - offset;
offset = t;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
LayoutParams params = (LayoutParams) child.getLayoutParams();
final int scrollMode = params.scrollMode;
switch (scrollMode) {
case LayoutParams.SCROLL_MODE_PIN:
child.setTranslationY(t);
break;
case LayoutParams.SCROLL_MODE_PARALLEX:
final float parallexMultiplier = params.parallexMultiplier;
child.setTranslationY(t * parallexMultiplier);
break;
default:
child.setTranslationY(0);
break;
}
}
updateScrimBounds();
collapsedBounds.offset(0, offsetBy);
expandedBounds.offset(0, offsetBy);
textHelper.setCollapsedBounds(collapsedBounds);
textHelper.setExpandedBounds(expandedBounds);
textHelper.offsetBounds(0, offsetBy);
final int toolbarBottom = toolbar.getBottom();
final int contentTop = contentView.getTop();
final int expanded = contentTop - toolbarBottom;
final float fraction = 1.0f * offset / expanded;
textHelper.setExpansionFraction(fraction);
final int toolbarHeight = toolbar.getHeight();
if (contentTop - offset < toolbarBottom + toolbarHeight) {
showScrims();
} else {
hideScrims();
}
invalidate();
}
};
private void updateScrimBounds() {
final int scrimTop = insetsTop + offset;
int scrimHeight = contentView.getTop() - scrimTop;
scrimHeight = Math.max(scrimHeight, toolbar.getHeight());
final int scrimBottom = scrimTop + scrimHeight;
contentScrim.setBounds(0, scrimTop, getWidth(), scrimBottom);
}
private void showScrims() {
if (!scrimsVisible) {
if (ViewCompat.isLaidOut(this) && !isInEditMode()) {
animateScrim(255);
} else {
setScrimAlpha(255);
}
scrimsVisible = true;
}
}
private void hideScrims() {
if (scrimsVisible) {
if (ViewCompat.isLaidOut(this) && !isInEditMode()) {
animateScrim(0);
} else {
setScrimAlpha(0);
}
scrimsVisible = false;
}
}
private void animateScrim(int targetAlpha) {
if (scrimAnimator == null) {
scrimAnimator = new ValueAnimator();
scrimAnimator.setDuration(SCRIM_ANIMATION_DURATION);
scrimAnimator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
scrimAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override public void onAnimationUpdate(ValueAnimator animator) {
setScrimAlpha((Integer) animator.getAnimatedValue());
}
});
} else if (scrimAnimator.isRunning()) {
scrimAnimator.cancel();
}
scrimAnimator.setIntValues(scrimAlpha, targetAlpha);
scrimAnimator.start();
}
private void setScrimAlpha(int alpha) {
if (alpha != scrimAlpha) {
final Drawable contentScrim = this.contentScrim;
if (contentScrim != null && toolbar != null) {
ViewCompat.postInvalidateOnAnimation(toolbar);
}
scrimAlpha = alpha;
ViewCompat.postInvalidateOnAnimation(AppBarRelativeLayout.this);
}
}
@Override public void draw(Canvas canvas) {
super.draw(canvas);
textHelper.draw(canvas);
if (statusBarScrim != null && scrimAlpha > 0) {
final int topInset = lastInsets != null ? lastInsets.getSystemWindowInsetTop() : 0;
if (topInset > 0) {
statusBarScrim.setBounds(0, offset, getWidth(), topInset + offset);
statusBarScrim.mutate().setAlpha(scrimAlpha);
statusBarScrim.draw(canvas);
}
}
}
@Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
if (child == toolbar) {
contentScrim.mutate().setAlpha(scrimAlpha);
contentScrim.draw(canvas);
} else if (child.getId() == R.id.backdrop) {
final int save = canvas.save();
canvas.clipRect(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
boolean result = super.drawChild(canvas, child, drawingTime);
canvas.restoreToCount(save);
return result;
}
return super.drawChild(canvas, child, drawingTime);
}
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (toolbar.getTop() < insetsTop) {
toolbar.offsetTopAndBottom(insetsTop - toolbar.getTop());
}
// TODO: Is the design lib way betteR?
dummyView.getDrawingRect(dummyBounds);
offsetDescendantRectToMyCoords(dummyView, dummyBounds);
updateScrimBounds();
collapsedBounds.left = dummyBounds.left;
collapsedBounds.top = dummyBounds.top;
collapsedBounds.right = dummyBounds.right;
collapsedBounds.bottom = dummyBounds.bottom;
collapsedBounds.offset(0, offset);
textHelper.setCollapsedBounds(collapsedBounds);
expandedBounds.left = left + expandedMarginLeft;
expandedBounds.top = dummyBounds.bottom + expandedMarginTop;
expandedBounds.right = (right - left) - expandedMarginRight;
expandedBounds.bottom = contentView.getTop() - expandedMarginBottom;
expandedBounds.offset(0, offset);
textHelper.setExpandedBounds(expandedBounds);
textHelper.recalculate();
}
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (dummyView == null) {
dummyView = new View(getContext());
toolbar.addView(dummyView, FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(super.generateDefaultLayoutParams());
}
@Override public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
public static class LayoutParams extends RelativeLayout.LayoutParams {
public static final int SCROLL_MODE_NONE = 0;
public static final int SCROLL_MODE_PIN = 1;
public static final int SCROLL_MODE_PARALLEX = 2;
private static final float DEFAULT_PARALLAX_MULTIPLIER = 0.0f;
int scrollMode = SCROLL_MODE_NONE;
float parallexMultiplier = DEFAULT_PARALLAX_MULTIPLIER;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.AppBar_LayoutParams);
scrollMode = a.getInt(R.styleable.AppBar_LayoutParams_layout_scrollMode, SCROLL_MODE_NONE);
setParallaxMultiplier(a.getFloat(R.styleable.AppBar_LayoutParams_layout_parallexMultiplier,
DEFAULT_PARALLAX_MULTIPLIER));
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams p) {
super(p);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(FrameLayout.LayoutParams source) {
super(source);
}
public void setScrollMode(int scrollMode) {
this.scrollMode = scrollMode;
}
public int getScrollMode() {
return scrollMode;
}
public void setParallaxMultiplier(float multiplier) {
parallexMultiplier = multiplier;
}
public float getParallaxMultiplier() {
return parallexMultiplier;
}
}
}