/*
* Copyright (C) 2014 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.ObjectAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.widget.ProgressBar;
import android.widget.TextView;
import butterknife.BindView;
import butterknife.ButterKnife;
import net.simonvt.cathode.R;
import net.simonvt.cathode.util.ViewUtils;
public class WatchingView extends ViewGroup {
public interface WatchingViewListener {
void onExpand(WatchingView view);
void onCollapse(WatchingView view);
void onEpisodeClicked(WatchingView view, long episodeId, String showTitle);
void onMovieClicked(WatchingView view, long id, String title, String overview);
void onAnimatingIn(WatchingView view);
void onAnimatingOut(WatchingView view);
}
public enum Type {
SHOW,
MOVIE
}
private static final int EXPAND_DURATION = 300;
private static final int EXPAND_DURATION_OFFSET = 0;
private static final int RADIUS_DURATION = 250;
private static final int RADIUS_DURATION_OFFSET = 300;
private static final int ANIMATION_DURATION =
Math.max(EXPAND_DURATION + EXPAND_DURATION_OFFSET, RADIUS_DURATION + RADIUS_DURATION_OFFSET);
private int maxWidth;
private boolean isExpanded;
private float animationProgress;
private int collapsedDiameter;
private int expandedDiameter;
private int diameter;
private int topBottomOffset;
private int collapsedRadius;
ObjectAnimator animator;
private Paint backgroundPaint = new Paint();
@BindView(R.id.poster) RemoteImageView posterView;
@BindView(R.id.infoParent) View infoParent;
@BindView(R.id.title) TextView titleView;
@BindView(R.id.progress) ProgressBar progress;
@BindView(R.id.subtitle) TextView subtitleView;
private static final boolean IS_LOLLIPOP = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
private WatchingViewListener watchingViewListener;
private Type type;
private long showId;
private String showTitle;
private long episodeId;
private String episodeTitle;
private long movieId;
private String movieTitle;
private String movieOverview;
private String poster;
private long startTime;
private long endTime;
private Handler handler;
private Runnable updateProgress = new Runnable() {
@Override public void run() {
progress.setProgress((int) (System.currentTimeMillis() - startTime));
handler.postDelayed(this, 30 * 1000L);
}
};
public WatchingView(Context context) {
super(context);
init(context);
}
public WatchingView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public WatchingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
setWillNotDraw(false);
backgroundPaint.setColor(0xFFFAFAFA);
collapsedDiameter = ViewUtils.dpToPx(context, 48);
expandedDiameter = ViewUtils.dpToPx(context, 16);
diameter = expandedDiameter;
if (IS_LOLLIPOP) {
initOutlineProvider();
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
if (getResources().getBoolean(R.bool.isTablet)) {
maxWidth = ViewUtils.dpToPx(context, 400);
}
animationProgress = 1.0f;
setVisibility(GONE);
handler = new Handler();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP) private void initOutlineProvider() {
setOutlineProvider(new ViewOutlineProvider() {
@Override public void getOutline(View view, Outline outline) {
Rect outlineRect = new Rect();
outlineRect.left = (int) (getPaddingLeft() + posterView.getTranslationX());
outlineRect.top = getPaddingTop() + topBottomOffset;
outlineRect.right = getWidth() - getPaddingRight();
outlineRect.bottom = getHeight() - getPaddingBottom() - topBottomOffset;
outline.setRoundRect(outlineRect, diameter / 2);
}
});
setClipToOutline(true);
}
public void setWatchingViewListener(WatchingViewListener watchingViewListener) {
this.watchingViewListener = watchingViewListener;
}
public void watchingShow(long showId, String showTitle, long episodeId, String episodeTitle,
String poster, long startTime, long endTime) {
if (type != Type.SHOW || showId != this.showId) {
clearVariables();
this.type = Type.SHOW;
this.showId = showId;
}
this.showTitle = showTitle;
this.episodeId = episodeId;
this.episodeTitle = episodeTitle;
this.poster = poster;
this.startTime = startTime;
this.endTime = endTime;
posterView.setImage(poster);
titleView.setText(showTitle);
subtitleView.setVisibility(VISIBLE);
subtitleView.setText(episodeTitle);
progress.setMax((int) (endTime - startTime));
progress.setProgress((int) (System.currentTimeMillis() - startTime));
animateIn();
}
public void watchingMovie(long movieId, String movieTitle, String overview, String poster,
long startTime, long endTime) {
if (type != Type.MOVIE || movieId != this.movieId) {
clearVariables();
this.type = Type.MOVIE;
this.movieId = movieId;
}
this.movieTitle = movieTitle;
this.movieOverview = overview;
this.poster = poster;
this.startTime = startTime;
this.endTime = endTime;
posterView.setImage(poster);
titleView.setText(movieTitle);
subtitleView.setVisibility(GONE);
progress.setMax((int) (endTime - startTime));
progress.setProgress((int) (System.currentTimeMillis() - startTime));
animateIn();
}
public void clearWatching() {
clearVariables();
animateOut();
}
private void clearVariables() {
type = null;
showId = -1L;
showTitle = null;
episodeId = -1L;
episodeTitle = null;
movieId = -1L;
movieTitle = null;
poster = null;
startTime = 0L;
endTime = 0L;
}
private void animateIn() {
if (watchingViewListener != null) {
watchingViewListener.onAnimatingIn(this);
}
animate().alpha(1.0f).withStartAction(new Runnable() {
@Override public void run() {
if (getVisibility() == GONE) {
setAlpha(0.0f);
setVisibility(VISIBLE);
}
}
});
}
private void animateOut() {
if (watchingViewListener != null) {
watchingViewListener.onAnimatingOut(this);
}
if (getVisibility() != GONE) {
animate().alpha(0.0f).withEndAction(new Runnable() {
@Override public void run() {
setVisibility(GONE);
collapse();
setAnimationProgress(1.0f);
}
});
}
}
private OnClickListener expandedClickListener = new OnClickListener() {
@Override public void onClick(View v) {
if (type == Type.SHOW) {
if (episodeId >= 0) {
watchingViewListener.onEpisodeClicked(WatchingView.this, episodeId, showTitle);
}
} else {
if (movieId >= 0) {
watchingViewListener.onMovieClicked(WatchingView.this, movieId, movieTitle,
movieOverview);
}
}
}
};
private OnClickListener collapsedClickListener = new OnClickListener() {
@Override public void onClick(View v) {
expand();
}
};
private void setIsExpanded(boolean isExpanded) {
this.isExpanded = isExpanded;
handler.removeCallbacks(updateProgress);
if (isExpanded) {
setOnClickListener(expandedClickListener);
posterView.setOnClickListener(null);
if (watchingViewListener != null) {
watchingViewListener.onExpand(this);
}
updateProgress.run();
} else {
setOnClickListener(null);
setClickable(false);
posterView.setOnClickListener(collapsedClickListener);
if (watchingViewListener != null) {
watchingViewListener.onCollapse(this);
}
}
}
public boolean isExpanded() {
return isExpanded;
}
public void expand() {
if (isExpanded()) {
return;
}
if (animator != null) {
animator.cancel();
animator = null;
}
animator = ObjectAnimator.ofFloat(WatchingView.this, "animationProgress", 0.0f);
animator.setDuration(ANIMATION_DURATION);
animator.start();
setIsExpanded(true);
}
public void collapse() {
if (!isExpanded) {
return;
}
if (animator != null) {
animator.cancel();
animator = null;
}
animator = ObjectAnimator.ofFloat(WatchingView.this, "animationProgress", 1.0f);
animator.setDuration(ANIMATION_DURATION);
animator.start();
setIsExpanded(false);
}
@Override protected void onFinishInflate() {
super.onFinishInflate();
ButterKnife.bind(this);
setIsExpanded(isExpanded);
}
@Override protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (isExpanded) {
handler.removeCallbacks(updateProgress);
handler.post(updateProgress);
}
}
@Override protected void onDetachedFromWindow() {
handler.removeCallbacks(updateProgress);
super.onDetachedFromWindow();
}
private void expandAnimation(float progress) {
final int width = getWidth();
final int pl = getPaddingLeft();
final int pr = getPaddingRight();
final int posterWidth = posterView.getWidth();
final int totalPosterOffset = width - pl - pr - posterWidth;
float offsetRatio = animationProgress * (EXPAND_DURATION + RADIUS_DURATION) / EXPAND_DURATION;
offsetRatio = Math.min(offsetRatio, 1.0f);
final int offset = (int) (totalPosterOffset * offsetRatio);
posterView.setTranslationX(offset);
infoParent.setTranslationX(offset);
infoParent.setAlpha(1.0f - progress);
}
private void radiusAnimation(float progress) {
final int posterWidth = posterView.getWidth();
final int posterHeight = posterView.getHeight();
final int maxRadius = posterWidth;
final int diff = (int) (progress * (maxRadius - expandedDiameter));
diameter = expandedDiameter + diff;
topBottomOffset = (int) ((posterHeight - posterWidth) * progress / 2);
}
public void setAnimationProgress(float animationProgress) {
this.animationProgress = animationProgress;
final float expandRatio = 1.0f * EXPAND_DURATION / ANIMATION_DURATION;
final float expandOffset = 1.0f * EXPAND_DURATION_OFFSET / ANIMATION_DURATION;
final float expandProgress =
Math.max(Math.min((animationProgress - expandOffset) / expandRatio, 1.0f), 0.0f);
expandAnimation(expandProgress);
final float radiusRatio = 1.0f * RADIUS_DURATION / ANIMATION_DURATION;
final float radiusOffset = 1.0f * RADIUS_DURATION_OFFSET / ANIMATION_DURATION;
final float radiusProgress =
Math.max(Math.min((animationProgress - radiusOffset) / radiusRatio, 1.0f), 0.0f);
radiusAnimation(radiusProgress);
invalidate();
invalidateOutlineCompat();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP) private void invalidateOutlineCompat() {
if (IS_LOLLIPOP) {
invalidateOutline();
}
}
public float getAnimationProgress() {
return animationProgress;
}
@Override protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (IS_LOLLIPOP) {
canvas.drawRect(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
getHeight() - getPaddingBottom(), backgroundPaint);
}
}
@Override protected void dispatchDraw(Canvas canvas) {
if (IS_LOLLIPOP) {
super.dispatchDraw(canvas);
} else {
Path clipPath = new Path();
final int width = getWidth();
final int height = getHeight();
final int posterOffset = (int) posterView.getTranslationX();
RectF topLeft = new RectF();
topLeft.left = getPaddingLeft() + posterOffset;
topLeft.top = getPaddingTop() + topBottomOffset;
topLeft.right = topLeft.left + diameter;
topLeft.bottom = topLeft.top + diameter;
RectF topRight = new RectF();
topRight.right = width - getPaddingRight();
topRight.top = getPaddingTop() + topBottomOffset;
topRight.left = topRight.right - diameter;
topRight.bottom = topRight.top + diameter;
RectF bottomRight = new RectF();
bottomRight.right = width - getPaddingRight();
bottomRight.bottom = height - getPaddingBottom() - topBottomOffset;
bottomRight.left = bottomRight.right - diameter;
bottomRight.top = bottomRight.bottom - diameter;
RectF bottomLeft = new RectF();
bottomLeft.left = getPaddingLeft() + posterOffset;
bottomLeft.bottom = height - getPaddingBottom() - topBottomOffset;
bottomLeft.right = bottomLeft.left + diameter;
bottomLeft.top = bottomLeft.bottom - diameter;
clipPath.moveTo(getPaddingLeft() + posterOffset,
getPaddingTop() + diameter + topBottomOffset);
clipPath.arcTo(topLeft, 180f, 90f, false);
clipPath.lineTo(width - getPaddingRight() - diameter, getPaddingTop() + topBottomOffset);
clipPath.arcTo(topRight, 270f, 90f, false);
clipPath.lineTo(width - getPaddingRight(),
height - getPaddingBottom() - diameter - topBottomOffset);
clipPath.arcTo(bottomRight, 0f, 90f, false);
clipPath.lineTo(getPaddingLeft() + diameter + posterOffset,
height - getPaddingBottom() - topBottomOffset);
clipPath.arcTo(bottomLeft, 90f, 90f, false);
clipPath.lineTo(getPaddingLeft() + posterOffset,
getPaddingTop() + diameter + topBottomOffset);
clipPath.close();
final int save = canvas.save();
canvas.clipPath(clipPath);
canvas.drawRect(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
getHeight() - getPaddingBottom(), backgroundPaint);
super.dispatchDraw(canvas);
canvas.restoreToCount(save);
}
}
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int height = b - t;
final int width = r - l;
LayoutParams posterParams = (LayoutParams) posterView.getLayoutParams();
LayoutParams infoParams = (LayoutParams) infoParent.getLayoutParams();
final int posterLeft = getPaddingLeft() + posterParams.leftMargin;
final int posterTop = getPaddingTop() + posterParams.topMargin;
final int posterRight = posterLeft + posterView.getMeasuredWidth();
final int posterBottom = posterTop + posterView.getMeasuredHeight();
posterView.layout(posterLeft, posterTop, posterRight, posterBottom);
final int infoLeft = posterRight + posterParams.rightMargin + infoParams.leftMargin;
final int infoRight = width - getPaddingRight() - infoParams.rightMargin;
final int infoHeight = infoParent.getMeasuredHeight();
final int infoTop = (height - infoHeight) / 2 + getPaddingTop();
final int infoBottom = infoTop + infoHeight;
infoParent.layout(infoLeft, infoTop, infoRight, infoBottom);
if (changed) {
setAnimationProgress(animationProgress);
}
}
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.UNSPECIFIED) {
throw new IllegalStateException("Must measure with an exact width");
}
int width = MeasureSpec.getSize(widthMeasureSpec);
if (maxWidth > 0) {
width = Math.min(width, maxWidth);
}
measureChild(posterView, widthMeasureSpec, heightMeasureSpec);
LayoutParams posterParams = (LayoutParams) posterView.getLayoutParams();
int leftoverWidth = width
- getPaddingLeft()
- posterParams.leftMargin
- posterView.getMeasuredWidth()
- posterParams.rightMargin
- getPaddingRight();
LayoutParams infoParams = (LayoutParams) infoParent.getLayoutParams();
int infoWidth = leftoverWidth - infoParams.leftMargin - infoParams.rightMargin;
final int infoWidthSpec = MeasureSpec.makeMeasureSpec(infoWidth, MeasureSpec.EXACTLY);
final int infoHeightSpec =
MeasureSpec.makeMeasureSpec(posterView.getMeasuredHeight(), MeasureSpec.AT_MOST);
infoParent.measure(infoWidthSpec, infoHeightSpec);
final int height = getPaddingTop()
+ posterParams.topMargin
+ posterView.getMeasuredHeight()
+ posterParams.bottomMargin
+ getPaddingBottom();
setMeasuredDimension(width, height);
}
@Override public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
@Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
public static class LayoutParams extends MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
@Override protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState state = new SavedState(superState);
state.type = type;
state.showId = showId;
state.showTitle = showTitle;
state.episodeId = episodeId;
state.episodeTitle = episodeTitle;
state.movieId = movieId;
state.movieTitle = movieTitle;
state.movieOverview = movieOverview;
state.poster = poster;
state.startTime = startTime;
state.endTime = endTime;
state.startTime = startTime;
state.endTime = endTime;
state.isExpanded = isExpanded();
return state;
}
@Override protected void onRestoreInstanceState(Parcelable state) {
SavedState savedState = (SavedState) state;
super.onRestoreInstanceState(savedState.getSuperState());
if (savedState.type == null) {
setVisibility(GONE);
} else {
setVisibility(VISIBLE);
}
if (savedState.type == Type.SHOW) {
watchingShow(savedState.showId, savedState.showTitle, savedState.episodeId,
savedState.episodeTitle, savedState.poster, savedState.startTime, savedState.endTime);
} else if (savedState.type == Type.MOVIE) {
watchingMovie(savedState.movieId, savedState.movieTitle, savedState.movieOverview,
savedState.poster, savedState.startTime, savedState.endTime);
}
setIsExpanded(savedState.isExpanded);
if (savedState.isExpanded) {
setAnimationProgress(0.0f);
} else {
setAnimationProgress(1.0f);
}
}
static class SavedState extends BaseSavedState {
private Type type;
private long showId;
private String showTitle;
private long episodeId;
private String episodeTitle;
private long movieId;
private String movieTitle;
private String movieOverview;
private String poster;
private long startTime;
private long endTime;
private boolean isExpanded;
public SavedState(Parcelable superState) {
super(superState);
}
public SavedState(Parcel in) {
super(in);
type = (Type) in.readSerializable();
showId = in.readLong();
showTitle = in.readString();
episodeId = in.readLong();
episodeTitle = in.readString();
movieId = in.readLong();
movieTitle = in.readString();
movieOverview = in.readString();
poster = in.readString();
startTime = in.readLong();
endTime = in.readLong();
isExpanded = in.readInt() == 1;
}
@Override public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeSerializable(type);
dest.writeLong(showId);
dest.writeString(showTitle);
dest.writeLong(episodeId);
dest.writeString(episodeTitle);
dest.writeLong(movieId);
dest.writeString(movieTitle);
dest.writeString(movieOverview);
dest.writeString(poster);
dest.writeLong(startTime);
dest.writeLong(endTime);
dest.writeInt(isExpanded ? 1 : 0);
}
@SuppressWarnings("UnusedDeclaration") public static final Creator<SavedState> CREATOR =
new Creator<SavedState>() {
@Override public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
@Override public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}