package me.ccrama.redditslide.ForceTouch;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
import android.support.annotation.FloatRange;
import android.support.annotation.IdRes;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import java.util.HashMap;
import jp.wasabeef.blurry.Blurry;
import me.ccrama.redditslide.ForceTouch.builder.PeekViewOptions;
import me.ccrama.redditslide.ForceTouch.callback.OnButtonUp;
import me.ccrama.redditslide.ForceTouch.callback.OnPeek;
import me.ccrama.redditslide.ForceTouch.callback.OnPop;
import me.ccrama.redditslide.ForceTouch.callback.OnRemove;
import me.ccrama.redditslide.ForceTouch.util.DensityUtils;
import me.ccrama.redditslide.ForceTouch.util.NavigationUtils;
import me.ccrama.redditslide.R;
import me.ccrama.redditslide.Reddit;
import me.ccrama.redditslide.Views.PeekMediaView;
import me.ccrama.redditslide.util.LogUtil;
public class PeekView extends FrameLayout {
private static final int ANIMATION_TIME = 300;
private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
private static final int FINGER_SIZE_DP = 40;
private int FINGER_SIZE;
public View content;
private ViewGroup.LayoutParams contentParams;
private View dim;
private PeekViewOptions options;
private int distanceFromTop;
private int distanceFromLeft;
private int screenWidth;
private int screenHeight;
private ViewGroup androidContentView = null;
private OnPeek callbacks;
private OnRemove remove;
public PeekView(Activity context, PeekViewOptions options, @LayoutRes int layoutRes,
@Nullable OnPeek callbacks) {
super(context);
init(context, options, LayoutInflater.from(context).inflate(layoutRes, this, false),
callbacks);
}
public void addButton(@IdRes Integer i, OnButtonUp onButtonUp) {
buttons.put(i, onButtonUp);
}
private OnPop mOnPop;
int currentHighlight;
static int eight = Reddit.dpToPxVertical(8);
public void highlightMenu(MotionEvent event) {
if(currentHighlight != 0){
final View v = content.findViewById(currentHighlight);
Rect outRect = new Rect();
v.getGlobalVisibleRect(outRect);
if(!outRect.contains((int) event.getX(), (int) event.getY())){
currentHighlight = 0;
ValueAnimator animator = ValueAnimator.ofInt(eight, eight * 2);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator){
v.setPadding(0, (Integer) valueAnimator.getAnimatedValue(), 0, (Integer) valueAnimator.getAnimatedValue());
}
});
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.setDuration(150);
animator.start();
} else {
return;
}
}
for (Integer i : buttons.keySet()) {
final View v = content.findViewById(i);
Rect outRect = new Rect();
v.getGlobalVisibleRect(outRect);
if(outRect.contains((int) event.getX(), (int) event.getY()) && i != currentHighlight){
currentHighlight = i;
ValueAnimator animator = ValueAnimator.ofInt(eight * 2, eight);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator){
v.setPadding(0, (Integer) valueAnimator.getAnimatedValue(), 0, (Integer) valueAnimator.getAnimatedValue());
}
});
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.setDuration(150);
animator.start();
break;
} else if(outRect.contains((int) event.getX(), (int) event.getY())){
break;
}
}
}
public void pop(){
if(mOnPop != null)
mOnPop.onPop();
}
public void setOnPop(OnPop mOnPop){
this.mOnPop = mOnPop;
}
public PeekView(Activity context, PeekViewOptions options, @NonNull View content,
@Nullable OnPeek callbacks) {
super(context);
init(context, options, content, callbacks);
}
HashMap<Integer, OnButtonUp> buttons = new HashMap<>();
public void checkButtons(MotionEvent event) {
for (Integer i : buttons.keySet()) {
View v = content.findViewById(i);
Rect outRect = new Rect();
v.getGlobalVisibleRect(outRect);
if(outRect.contains((int) event.getX(), (int) event.getY())){
buttons.get(i).onButtonUp();
}
}
}
public void doScroll(MotionEvent event) {
((PeekMediaView)content.findViewById(R.id.peek)).doScroll(event);
}
private void init(Activity context, PeekViewOptions options, @NonNull View content,
@Nullable OnPeek callbacks) {
this.options = options;
this.callbacks = callbacks;
FINGER_SIZE = DensityUtils.toPx(context, FINGER_SIZE_DP);
// get the main content view of the display
androidContentView = (FrameLayout) context.findViewById(android.R.id.content).getRootView();
// initialize the display size
Display display = context.getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
screenHeight = size.y;
screenWidth = size.x;
// set up the content we want to show
this.content = content;
contentParams = content.getLayoutParams();
if (options.getAbsoluteHeight() != 0) {
setHeight(DensityUtils.toPx(context, options.getAbsoluteHeight()));
} else {
setHeightByPercent(options.getHeightPercent());
}
if (options.getAbsoluteWidth() != 0) {
setWidth(DensityUtils.toPx(context, options.getAbsoluteWidth()));
} else {
setWidthByPercent(options.getWidthPercent());
}
// tell the code that the view has been onInflated and let them use it to
// set up the layout.
if (callbacks != null) {
callbacks.onInflated(this, content);
}
// add the background dim to the frame
dim = new View(context);
dim.setBackgroundColor(Color.BLACK);
dim.setAlpha(options.getBackgroundDim());
LayoutParams dimParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
dim.setLayoutParams(dimParams);
if (options.shouldBlurBackground()) {
try {
Blurry.with(context)
.radius(2)
.sampling(5)
.animate()
.color(options.getBlurOverlayColor())
.onto((ViewGroup) androidContentView.getRootView());
dim.setAlpha(0f);
} catch(Exception ignored){
}
}
// add the dim and the content view to the upper level frame layout
addView(dim);
addView(content);
}
/**
* Sets how far away from the top of the screen the view should be displayed. Distance should be
* the value in PX.
*
* @param distance the distance from the top in px.
*/
private void setDistanceFromTop(int distance) {
this.distanceFromTop = options.fullScreenPeek() ? 0 : distance;
}
/**
* Sets how far away from the left side of the screen the view should be displayed. Distance
* should be the value in PX.
*
* @param distance the distance from the left in px.
*/
private void setDistanceFromLeft(int distance) {
this.distanceFromLeft = options.fullScreenPeek() ? 0 : distance;
}
/**
* Sets the width of the view in PX.
*
* @param width the width of the circle in px
*/
private void setWidth(int width) {
contentParams.width = options.fullScreenPeek() ? screenWidth : width;
content.setLayoutParams(contentParams);
}
/**
* Sets the height of the view in PX.
*
* @param height the height of the circle in px
*/
private void setHeight(int height) {
contentParams.height = options.fullScreenPeek() ? screenHeight : height;
content.setLayoutParams(contentParams);
}
/**
* Sets the width of the window according to the screen width.
*
* @param percent of screen width
*/
public void setWidthByPercent(@FloatRange(from = 0, to = 1) float percent) {
setWidth((int) (screenWidth * percent));
}
/**
* Sets the height of the window according to the screen height.
*
* @param percent of screen height
*/
public void setHeightByPercent(@FloatRange(from = 0, to = 1) float percent) {
setHeight((int) (screenHeight * percent));
}
/**
* Places the peek view over the top of a motion event. This will translate the motion event's
* start points so that the PeekView isn't covered by the finger.
*
* @param event event that activates the peek view
*/
public void setOffsetByMotionEvent(MotionEvent event) {
int x = (int) event.getRawX();
int y = (int) event.getRawY();
if (x + contentParams.width + FINGER_SIZE < screenWidth) {
setContentOffset(x, y, Translation.HORIZONTAL, FINGER_SIZE);
} else if (x - FINGER_SIZE - contentParams.width > 0) {
setContentOffset(x, y, Translation.HORIZONTAL, -1 * FINGER_SIZE);
} else if (y + contentParams.height + FINGER_SIZE < screenHeight) {
setContentOffset(x, y, Translation.VERTICAL, FINGER_SIZE);
} else if (y - FINGER_SIZE - contentParams.height > 0) {
setContentOffset(x, y, Translation.VERTICAL, -1 * FINGER_SIZE);
} else {
// it won't fit anywhere
if (x < screenWidth / 2) {
setContentOffset(x, y, Translation.HORIZONTAL, FINGER_SIZE);
} else {
setContentOffset(x, y, Translation.HORIZONTAL, -1 * FINGER_SIZE);
}
}
}
/**
* Show the PeekView over the point of motion
*
* @param startX
* @param startY
*/
private void setContentOffset(int startX, int startY, Translation translation,
int movementAmount) {
if (translation == Translation.VERTICAL) {
// center the X around the start point
int originalStartX = startX;
startX -= contentParams.width / 2;
// if Y is in the lower half, we want it to go up, otherwise, leave it the same
boolean moveDown = true;
if (startY + contentParams.height + FINGER_SIZE > screenHeight) {
startY -= contentParams.height;
moveDown = false;
if (movementAmount > 0) {
movementAmount *= -1;
}
}
// when moving the peek view below the finger location, we want to offset it a bit to the right
// or left as well, just so the hand doesn't cover it up.
int extraXOffset = 0;
if (moveDown) {
extraXOffset = DensityUtils.toPx(getContext(), 200);
if (originalStartX > screenWidth / 2) {
extraXOffset = extraXOffset * -1; // move it a bit to the left
}
}
// make sure they aren't outside of the layout bounds and move them with the movementAmount
// I move the x just a bit to the right or left here as well, because it just makes things look better
startX = ensureWithinBounds(startX + extraXOffset, screenWidth, contentParams.width);
startY =
ensureWithinBounds(startY + movementAmount, screenHeight, contentParams.height);
} else {
// center the Y around the start point
startY -= contentParams.height / 2;
// if X is in the right half, we want it to go left
if (startX + contentParams.width + FINGER_SIZE > screenWidth) {
startX -= contentParams.width;
if (movementAmount > 0) {
movementAmount *= -1;
}
}
// make sure they aren't outside of the layout bounds and move them with the movementAmount
startX = ensureWithinBounds(startX + movementAmount, screenWidth, contentParams.width);
startY = ensureWithinBounds(startY, screenHeight, contentParams.height);
}
// check to see if the system bars are covering anything
int statusBar = NavigationUtils.getStatusBarHeight(getContext());
if (startY < statusBar) { // if it is above the status bar and action bar
startY = statusBar + 10;
} else if (NavigationUtils.hasNavBar(getContext())
&& startY + contentParams.height > screenHeight - NavigationUtils.getNavBarHeight(
getContext())) {
// if there is a nav bar and the popup is underneath it
startY = screenHeight - contentParams.height - NavigationUtils.getNavBarHeight(
getContext()) - DensityUtils.toDp(getContext(), 10);
} else if (!NavigationUtils.hasNavBar(getContext())
&& startY + contentParams.height > screenHeight) {
startY = screenHeight - contentParams.height - DensityUtils.toDp(getContext(), 10);
}
// set the newly computed distances from the start and top sides
setDistanceFromLeft(startX);
setDistanceFromTop(startY);
}
private int ensureWithinBounds(int value, int screenSize, int contentSize) {
// check these against the layout bounds
if (value < 0) {
// if it is off the left side
value = 10;
} else if (value > screenSize - contentSize) {
// if it is off the right side
value = screenSize - contentSize - 10;
}
return value;
}
/**
* Show the content of the PeekView by adding it to the android.R.id.content FrameLayout.
*/
public void show() {
androidContentView.addView(this);
// set the translations for the content view
content.setTranslationX(distanceFromLeft);
content.setTranslationY(distanceFromTop);
// animate the alpha of the PeekView
ObjectAnimator animator = ObjectAnimator.ofFloat(this, View.ALPHA, 0.0f, 1.0f);
animator.addListener(new AnimatorEndListener() {
@Override
public void onAnimationEnd(Animator animator) {
if (callbacks != null) {
callbacks.shown();
}
}
});
animator.setDuration(options.useFadeAnimation() ? ANIMATION_TIME : 0);
animator.setInterpolator(INTERPOLATOR);
animator.start();
}
/**
* Hide the PeekView and remove it from the android.R.id.content FrameLayout.
*/
public void hide() {
// animate with a fade
ObjectAnimator animator = ObjectAnimator.ofFloat(this, View.ALPHA, 1.0f, 0.0f);
animator.addListener(new AnimatorEndListener() {
@Override
public void onAnimationEnd(Animator animator) {
// remove the view from the screen
androidContentView.removeView(PeekView.this);
if (callbacks != null) {
callbacks.dismissed();
}
}
});
animator.setDuration(options.useFadeAnimation() ? ANIMATION_TIME : 0);
animator.setInterpolator(INTERPOLATOR);
animator.start();
Blurry.delete((ViewGroup) androidContentView.getRootView());
if(remove != null)
remove.onRemove();
}
public void setOnRemoveListener(OnRemove onRemove){
this.remove = onRemove;
}
/**
* Wrapper class so we only have to implement the onAnimationEnd method.
*/
private abstract class AnimatorEndListener implements Animator.AnimatorListener {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
}
private enum Translation {HORIZONTAL, VERTICAL}
}