package com.klinker.android.peekview; import android.animation.Animator; import android.animation.ObjectAnimator; import android.app.Activity; import android.graphics.Color; import android.graphics.Point; import android.support.annotation.FloatRange; 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.DecelerateInterpolator; import android.view.animation.Interpolator; import android.widget.FrameLayout; import com.klinker.android.peekview.builder.PeekViewOptions; import com.klinker.android.peekview.callback.OnPeek; import com.klinker.android.peekview.util.DensityUtils; import com.klinker.android.peekview.util.NavigationUtils; import jp.wasabeef.blurry.Blurry; 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; private 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; 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 PeekView(Activity context, PeekViewOptions options, @NonNull View content, @Nullable OnPeek callbacks) { super(context); init(context, options, content, callbacks); } 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(content); } // add the background dim to the frame dim = new View(context); dim.setBackgroundColor(Color.BLACK); dim.setAlpha(options.getBackgroundDim()); FrameLayout.LayoutParams dimParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); dim.setLayoutParams(dimParams); if (options.shouldBlurBackground()) { Blurry.with(context) .radius(2) .sampling(5) .animate() .color(options.getBlurOverlayColor()) .onto((ViewGroup) androidContentView.getRootView()); dim.setAlpha(0f); } // 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()); } /** * 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 } }