package com.premnirmal.Magnet; import android.content.Context; import android.graphics.PixelFormat; import android.os.Handler; import android.os.Looper; import android.util.DisplayMetrics; import android.view.GestureDetector; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; /** * Created by prem on 7/20/14. * Desc: Class holding the Magnet Icon, and performing touchEvents on the view. */ public class Magnet implements View.OnTouchListener { protected static final int TOUCH_TIME_THRESHOLD = 200; protected View mIconView; protected RemoveView mRemoveView; protected WindowManager mWindowManager; protected WindowManager.LayoutParams mLayoutParams; protected Context mContext; protected GestureDetector mGestureDetector; protected boolean shouldStickToWall = true; protected boolean shouldFlingAway = true; protected IconCallback mListener; protected MoveAnimator mAnimator; protected long lastTouchDown; protected float lastXPose, lastYPose; protected boolean isBeingDragged = false; protected int mWidth, mHeight; protected int mIconWidth = -1, mIconHeight = -1; protected int mInitialX = -1, mInitialY = -1; public static Builder<Magnet> newBuilder(Context context) { return new MagnetBuilder(context); } private static class MagnetBuilder extends Builder<Magnet> { MagnetBuilder(Context context) { super(Magnet.class, context); } @Override public Magnet build() { return super.build(); } } /** * Builder class to create your {@link Magnet} */ public static class Builder<T extends Magnet> { protected T magnet; /** * Used to instantiate your subclass of {@link Magnet} * * @param clazz your subclass */ public Builder(Class<T> clazz, Context context) { final Constructor<T> constructor; try { constructor = clazz.getDeclaredConstructor(Context.class); constructor.setAccessible(true); magnet = constructor.newInstance(context); } catch (NoSuchMethodException e) { e.printStackTrace(); throw new RuntimeException(e); } catch (IllegalAccessException e) { e.printStackTrace(); throw new RuntimeException(e); } catch (InstantiationException e) { e.printStackTrace(); throw new RuntimeException(e); } catch (InvocationTargetException e) { e.printStackTrace(); throw new RuntimeException(e); } } /** * The Icon must have a view, provide a view or a layout using {@link #setIconView(int)} * * @param iconView the view representing the icon */ public Builder<T> setIconView(View iconView) { magnet.mIconView = iconView; magnet.mIconView.setOnTouchListener(magnet); return this; } /** * Use an xml layout to provide the button view * * @param iconViewRes the layout id of the icon */ public Builder<T> setIconView(int iconViewRes) { magnet.mIconView = LayoutInflater.from(magnet.mContext).inflate(iconViewRes, null); magnet.mIconView.setOnTouchListener(magnet); return this; } /** * whether your magnet sticks to the edge of your screen when you release it */ public Builder<T> setShouldStickToWall(boolean shouldStick) { magnet.shouldStickToWall = shouldStick; return this; } /** * whether you can fling away your Magnet towards the bottom of the screen */ public Builder<T> setShouldFlingAway(boolean shoudlFling) { magnet.shouldFlingAway = shoudlFling; return this; } /** * Callback for when the icon moves, or when it isis flung away and destroyed */ public Builder<T> setIconCallback(IconCallback callback) { magnet.mListener = callback; return this; } /** * * @param shouldBeResponsive * @return */ public Builder<T> setRemoveIconShouldBeResponsive(boolean shouldBeResponsive) { magnet.mRemoveView.shouldBeResponsive = shouldBeResponsive; return this; } /** * you can set a custom remove icon or use the default one */ public Builder<T> setRemoveIconResId(int removeIconResId) { magnet.mRemoveView.setIconResId(removeIconResId); return this; } /** * you can set a custom remove icon shadow or use the default one */ public Builder<T> setRemoveIconShadow(int shadow) { magnet.mRemoveView.setShadowBG(shadow); return this; } /** * Set the initial coordinates of the magnet */ public Builder<T> setInitialPosition(int x, int y) { magnet.mInitialX = x; magnet.mInitialY = y; return this; } /** * Set a custom width for the icon view. default is {@link WindowManager.LayoutParams#WRAP_CONTENT} */ public Builder<T> setIconWidth(int width) { magnet.mIconWidth = width; return this; } /** * * Set a custom height for the icon view. default is {@link WindowManager.LayoutParams#WRAP_CONTENT} */ public Builder<T> setIconHeight(int height) { magnet.mIconHeight = height; return this; } public T build() { if (magnet.mIconView == null) { throw new NullPointerException("Magnet view is null! Must set a view for the magnet!"); } return magnet; } } public Magnet(Context context) { mContext = context; mGestureDetector = new GestureDetector(context, new FlingListener()); mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mAnimator = new MoveAnimator(); mRemoveView = new RemoveView(context); } /** * Show the Magnet i.e. add it to the Window */ public void show() { addToWindow(mIconView); updateSize(); if (mInitialX != -1 || mInitialY != -1) { setPosition(mInitialX, mInitialY, true); } else { goToWall(); } } protected void addToWindow(View icon) { WindowManager.LayoutParams params = new WindowManager.LayoutParams( mIconWidth > 0 ? mIconWidth : WindowManager.LayoutParams.WRAP_CONTENT, mIconHeight > 0 ? mIconHeight : WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT); mWindowManager.addView(icon, mLayoutParams = params); } protected void updateSize() { final DisplayMetrics metrics = new DisplayMetrics(); mWindowManager.getDefaultDisplay().getMetrics(metrics); mWidth = (metrics.widthPixels - (mIconWidth > 0 ? mIconWidth : mIconView.getWidth())) / 2; mHeight = (metrics.heightPixels - (mIconHeight > 0 ? mIconHeight : mIconView.getHeight())) / 2; } @Override public boolean onTouch(View view, MotionEvent event) { boolean eaten = false; if (shouldFlingAway) { eaten = mGestureDetector.onTouchEvent(event); } if (eaten) { flingAway(); } else { float x = event.getRawX(); float y = event.getRawY(); int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { showRemoveView(); lastTouchDown = System.currentTimeMillis(); mAnimator.stop(); updateSize(); isBeingDragged = true; } else if (action == MotionEvent.ACTION_UP) { if (System.currentTimeMillis() - lastTouchDown < TOUCH_TIME_THRESHOLD) { if (mListener != null) { mListener.onIconClick(mIconView, x, y); } } hideRemoveView(); isBeingDragged = false; eaten = false; goToWall(); } else if (action == MotionEvent.ACTION_MOVE) { if (isBeingDragged) { move(x - lastXPose, y - lastYPose); } } lastXPose = x; lastYPose = y; } return eaten; } protected void flingAway() { if (shouldFlingAway) { int y = mContext.getResources().getDisplayMetrics().heightPixels / 2; int x = 0; mAnimator.start(x, y); if (mListener != null) { mListener.onFlingAway(); } destroy(); } } protected void showRemoveView() { if (mRemoveView != null && shouldFlingAway) { mRemoveView.show(); } } protected void hideRemoveView() { if (mRemoveView != null && shouldFlingAway) { mRemoveView.hide(); } } protected void goToWall() { if (shouldStickToWall) { float nearestXWall = mLayoutParams.x >= 0 ? mWidth : -mWidth; float nearestYWall = mLayoutParams.y > 0 ? mHeight : -mHeight; if (Math.abs(mLayoutParams.x - nearestXWall) < Math.abs(mLayoutParams.y - nearestYWall)) { mAnimator.start(nearestXWall, mLayoutParams.y); } else { mAnimator.start(mLayoutParams.x, nearestYWall); } } } protected void move(float deltaX, float deltaY) { mLayoutParams.x += deltaX; mLayoutParams.y += deltaY; if (mRemoveView != null && shouldFlingAway) { mRemoveView.onMove(mLayoutParams.x, mLayoutParams.y); } mWindowManager.updateViewLayout(mIconView, mLayoutParams); if (mListener != null) { mListener.onMove(mLayoutParams.x, mLayoutParams.y); } if (shouldFlingAway && !isBeingDragged && Math.abs(mLayoutParams.x) < (mIconWidth > 0 ? mIconWidth : 50) && Math.abs( mLayoutParams.y - (mContext.getResources().getDisplayMetrics().heightPixels / 2)) < ( mIconHeight > 0 ? mIconHeight * 3 : 250)) { flingAway(); } } /** * Destroys the magnet - removes the view from the WindowManager and calls * {@link IconCallback#onIconDestroyed()} */ public void destroy() { mWindowManager.removeView(mIconView); if (mRemoveView != null) { mRemoveView.destroy(); } if (mListener != null) { mListener.onIconDestroyed(); } mContext = null; } /** * Set the position of the Magnet. * Note: must be called **after** {@link #show()} is called. * This will call {@link IconCallback#onMove(float, float)} on your listener * * @param x the x position * @param y the y position * @param animate whether the Magnet should animate to that position. If false the Magnet * will simply just set its coordinates to the given position */ public void setPosition(int x, int y, boolean animate) { if (animate) { mAnimator.start(x, y); } else { mLayoutParams.x = x; mLayoutParams.y = y; mWindowManager.updateViewLayout(mIconView, mLayoutParams); if (mListener != null) { mListener.onMove(mLayoutParams.x, mLayoutParams.y); } } } /** * Update the icon view size after the magnet has been shown * @param width the width of the icon view * @param height the height of the icon view */ public void setIconSize(int width, int height) { mIconWidth = width; mIconHeight = height; mLayoutParams.width = width; mLayoutParams.height = height; mWindowManager.updateViewLayout(mIconView, mLayoutParams); } public class MoveAnimator implements Runnable { protected Handler handler = new Handler(Looper.getMainLooper()); public float destinationX; public float destinationY; public long startingTime; public void start(float x, float y) { this.destinationX = x; this.destinationY = y; startingTime = System.currentTimeMillis(); handler.post(this); } @Override public void run() { if (mIconView != null && mIconView.getParent() != null) { float progress = Math.min(1, (System.currentTimeMillis() - startingTime) / 400f); float deltaX = (destinationX - mLayoutParams.x) * progress; float deltaY = (destinationY - mLayoutParams.y) * progress; move(deltaX, deltaY); if (progress < 1) { handler.post(this); } } } public void stop() { handler.removeCallbacks(this); } } }