diff --git a/README.md b/README.md index 48258d4..6256316 100644 --- a/README.md +++ b/README.md @@ -36,23 +36,27 @@ Here's a quick demo video showing this library in action: } dependencies { - compile 'com.github.danialgoodwin:android-global-overlay:v0.8' + compile 'com.github.danialgoodwin:android-global-overlay:v0.9 } 2. Subclass `GlobalOverlayService` and call `addOverlayView(View)`. Example working code: - public class MySimpleOverlayService extends GlobalOverlayService { + public class MySimpleOverlayService extends Service { + + private GlobalOverlay mGlobalOverlay; @Override public void onCreate() { super.onCreate(); + mGlobalOverlay = new GlobalOverlay(this); ImageView view = new ImageView(this); view.setImageResource(R.mipmap.ic_launcher); - addOverlayView(view, new View.OnClickListener() { + mGlobalOverlay.addOverlayView(view, new View.OnClickListener() { @Override public void onClick(View v) { toast("onClick"); + stopSelf(); // Stop service not needed. } }); } @@ -85,11 +89,9 @@ Here's a quick demo video showing this library in action: Done. Most cases won't need to do any more than that. Though there are a few more methods to support other use cases. -`GlobalOverlayService` will handle everything else, including closing the overlay view when the `Service` is destroyed. - -The library class `GlobalOverlayService` has a few more features available: +The library class `GlobalOverlay` has a few more features available: - In the above sample, where the `OnClickListener` is passed in as an argument, there's an overloaded method that also allows an `OnLongClickListener` and `OnOverlayRemoveListener`. (Note: OnLongClickListener isn't implemented yet.) -- Change what the "remove view" looks like by overriding `onGetRemoveView()`. +- Change what the "remove view" looks by using the overloaded constructor `GlobalOverlay(Context, View)`. - You can remove the overlay without destroying the `Service` by calling `removeOverlayView(View)` with the same view you used in `addOverlayView(View)`. - The `OnRemoveOverlayListener.onRemoveOverlay` provides an argument that takes into account whether or not the user is the one to remove the overlay. (I'm using this info for analytics) @@ -104,9 +106,8 @@ The library class `GlobalOverlayService` has a few more features available: ## Possible Gotchas ## -- When the user manually removes the overlay (by dragging to the removeView), the service will be destroyed. +- When the user manually removes the overlay (by dragging to the removeView), you would have to manually stop the service is it's not needed anymore. - Currently, only one floating overlay view is allowed at a time. -- If overriding `Service.onCreate()`, then make sure to call `super.onCreate()` so that `GlobalOverlayService` can do some required setup. @@ -130,10 +131,7 @@ The current implementation is set in `addOverlayView()`, which calls `newSimpleO ### API Design considerations ### -- For my simple case, I didn't need to bind the service, so `GlobalOverlayService` implements `Service.onBind()` by always returning null. -- I was thinking about making a `protected abstract View mGlobalOverlayService.onCreateView()` so that implementers didn't have to worry about when to call `addOverlayView()` nor the `Service` lifecycle. But, as a trade-off it would be limiting for those who may not want an overlay for the entire life of the `Service`. - - +The first version of this library extended `Service`. Since v0.9, the main library has been changed to be a regular object that was more versatile with only a few more lines for the API implementer. For a while, the old `GlobalOverlayService` will remain in the repo. diff --git a/demo/build.gradle b/demo/build.gradle index f8c2da0..e9eca89 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -31,5 +31,5 @@ dependencies { compile project(':globaloverlay') // Use the following in your own project since it's not local. -// compile 'com.github.danialgoodwin:android-global-overlay:v0.8' +// compile 'com.github.danialgoodwin:android-global-overlay:v0.9' } diff --git a/demo/src/main/java/com/danialgoodwin/globaloverlay/demo/SimpleOverlayService.java b/demo/src/main/java/com/danialgoodwin/globaloverlay/demo/SimpleOverlayService.java index 2df8fd3..1863c46 100644 --- a/demo/src/main/java/com/danialgoodwin/globaloverlay/demo/SimpleOverlayService.java +++ b/demo/src/main/java/com/danialgoodwin/globaloverlay/demo/SimpleOverlayService.java @@ -3,22 +3,30 @@ */ package com.danialgoodwin.globaloverlay.demo; +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; import android.view.View; import android.widget.ImageView; import android.widget.Toast; +import com.danialgoodwin.globaloverlay.GlobalOverlay; import com.danialgoodwin.globaloverlay.GlobalOverlayService; /** Simple demo for creating an interactable global floating view. */ -public class SimpleOverlayService extends GlobalOverlayService { +public class SimpleOverlayService extends Service { + + private GlobalOverlay mGlobalOverlay; @Override public void onCreate() { super.onCreate(); + mGlobalOverlay = new GlobalOverlay(this); + ImageView view = new ImageView(this); view.setImageResource(R.mipmap.ic_launcher); - addOverlayView(view, new View.OnClickListener() { + mGlobalOverlay.addOverlayView(view, new View.OnClickListener() { @Override public void onClick(View v) { toast("onClick"); @@ -29,14 +37,20 @@ public boolean onLongClick(View v) { toast("onLongClick not implemented yet"); return false; } - }, new OnRemoveOverlayListener() { + }, new GlobalOverlay.OnRemoveOverlayListener() { @Override - public void onRemoveOverlay(View view, boolean b) { + public void onRemoveOverlay(View view, boolean isRemovedByUser) { toast("onRemoveOverlay"); + stopSelf(); } }); } + @Override + public IBinder onBind(Intent intent) { + return null; + } + private void toast(String message) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } diff --git a/demo/src/main/java/com/danialgoodwin/globaloverlay/demo/SimpleOverlayServiceOld.java b/demo/src/main/java/com/danialgoodwin/globaloverlay/demo/SimpleOverlayServiceOld.java new file mode 100644 index 0000000..88bea03 --- /dev/null +++ b/demo/src/main/java/com/danialgoodwin/globaloverlay/demo/SimpleOverlayServiceOld.java @@ -0,0 +1,44 @@ +/** + * Created by Danial on 3/16/2015. + */ +package com.danialgoodwin.globaloverlay.demo; + +import android.view.View; +import android.widget.ImageView; +import android.widget.Toast; + +import com.danialgoodwin.globaloverlay.GlobalOverlayService; + +/** Simple demo for creating an interactable global floating view. */ +public class SimpleOverlayServiceOld extends GlobalOverlayService { + + @Override + public void onCreate() { + super.onCreate(); + + ImageView view = new ImageView(this); + view.setImageResource(R.mipmap.ic_launcher); + addOverlayView(view, new View.OnClickListener() { + @Override + public void onClick(View v) { + toast("onClick"); + } + }, new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + toast("onLongClick not implemented yet"); + return false; + } + }, new OnRemoveOverlayListener() { + @Override + public void onRemoveOverlay(View view, boolean b) { + toast("onRemoveOverlay"); + } + }); + } + + private void toast(String message) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); + } + +} diff --git a/globaloverlay/build.gradle b/globaloverlay/build.gradle index f569fab..c548ef7 100644 --- a/globaloverlay/build.gradle +++ b/globaloverlay/build.gradle @@ -9,7 +9,7 @@ android { minSdkVersion 15 targetSdkVersion 22 versionCode 1 - versionName "0.8" + versionName "0.9" } buildTypes { diff --git a/globaloverlay/src/main/java/com/danialgoodwin/globaloverlay/GlobalOverlay.java b/globaloverlay/src/main/java/com/danialgoodwin/globaloverlay/GlobalOverlay.java new file mode 100644 index 0000000..64a6598 --- /dev/null +++ b/globaloverlay/src/main/java/com/danialgoodwin/globaloverlay/GlobalOverlay.java @@ -0,0 +1,219 @@ +/** + * Created by Danial on 3/18/2015. + */ +package com.danialgoodwin.globaloverlay; + + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.PixelFormat; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.WindowManager; + +public class GlobalOverlay { + private static final String LOGCAT_TAG = "GlobalOverlay"; + private static void log(String message) { + Log.d(LOGCAT_TAG, message); + } + + private Context mContext; + private WindowManager mWindowManager; + private View mRemoveView; + private View mOverlayView; + + private View.OnTouchListener mOnTouchListener; + private View.OnClickListener mOnClickListener; + private View.OnLongClickListener mOnLongClickListener; + private OnRemoveOverlayListener mOnRemoveOverlayListener; + private WindowManager.LayoutParams mOverlayLayoutParams; + + public GlobalOverlay(Context context) { + this(context, newRemoveView(context)); + } + + public GlobalOverlay(Context context, View removeView) { + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + mContext = context; + mRemoveView = removeView; + setupRemoveView(mRemoveView); + } + + /** Return the view to use for the "remove view" that appears at the bottom of the screen. + * Override this to change the image for the remove view. Returning null will throw a + * NullPointerException in a subsequent method.*/ + @SuppressLint("InflateParams") + private static View newRemoveView(Context context) { + return LayoutInflater.from(context).inflate(R.layout.overlay_remove_view, null); + } + + /** Sets this view to the bottom of the screen and only visible when user is dragging an + * overlay view. This modifies the instance passed in. */ + private void setupRemoveView(View removeView) { + removeView.setVisibility(View.GONE); + mWindowManager.addView(removeView, newWindowManagerLayoutParamsForRemoveView()); + } + + /** Add a global floating view. + * + * @param view the view to overlay across all apps and activities + * @param onClickListener get notified of a click, set null to ignore + */ + public final void addOverlayView(View view, View.OnClickListener onClickListener) { + addOverlayView(view, onClickListener, null, null); + } + + /** Add a global floating view. + * + * @param view the view to overlay across all apps and activities + * @param onClickListener get notified of a click, set null to ignore + * @param onLongClickListener not implemented yet, just set as null + * @param onRemoveOverlayListener get notified when overlay is removed (not from a destroyed service though) + */ + public final void addOverlayView(View view, View.OnClickListener onClickListener, + View.OnLongClickListener onLongClickListener, OnRemoveOverlayListener onRemoveOverlayListener) { + mOverlayView = view; + mOnClickListener = onClickListener; + mOnLongClickListener = onLongClickListener; + mOnRemoveOverlayListener = onRemoveOverlayListener; + mOverlayLayoutParams = newWindowManagerLayoutParams(); + + mOnTouchListener = newSimpleOnTouchListener(); + mOverlayView.setOnTouchListener(mOnTouchListener); + + mWindowManager.addView(mOverlayView, newWindowManagerLayoutParams()); + } + + /** Manually remove an overlay without destroying the service. */ + public final void removeOverlayView(View view) { + removeOverlayView(view, false); + } + + /** Remove a overlay without destroying the service. */ + public final void removeOverlayView(View view, boolean isRemovedByUser) { + if (view != null) { + if (mOnRemoveOverlayListener != null) { + mOnRemoveOverlayListener.onRemoveOverlay(view, isRemovedByUser); + } + mWindowManager.removeView(view); + } + } + + /** Remove all views. This instance becomes unusable after calling this. */ +// public void destroy() { +// // TODO: Remove all views, when it's possible to have multiple overlays. +// } + + /** Provides the drag ability for the overlay view. This touch listener + * allows user to drag the view anywhere on screen. */ + private View.OnTouchListener newSimpleOnTouchListener() { + return new View.OnTouchListener() { + // private long timeStart; // Maybe use in the future, with ViewConfiguration's getLongClickTime or whatever it is called. + private int initialX; + private int initialY; + private float initialTouchX; + private float initialTouchY; + private int[] overlayViewLocation = {0,0}; + + private boolean isOverRemoveView; + private int[] removeViewLocation = {0,0}; + + private final int touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); + + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: +// timeStart = System.currentTimeMillis(); + initialX = mOverlayLayoutParams.x; + initialY = mOverlayLayoutParams.y; + initialTouchX = event.getRawX(); + initialTouchY = event.getRawY(); + + mRemoveView.setVisibility(View.VISIBLE); + return true; + case MotionEvent.ACTION_MOVE: + mOverlayLayoutParams.x = initialX + (int) (event.getRawX() - initialTouchX); + mOverlayLayoutParams.y = initialY + (int) (event.getRawY() - initialTouchY); + mWindowManager.updateViewLayout(mOverlayView, mOverlayLayoutParams); + + mOverlayView.getLocationOnScreen(overlayViewLocation); + mRemoveView.getLocationOnScreen(removeViewLocation); + isOverRemoveView = isPointInArea(overlayViewLocation[0], overlayViewLocation[1], + removeViewLocation[0], removeViewLocation[1], mRemoveView.getWidth()); + if (isOverRemoveView) { + // TODO: Maybe, make it look like the overlay view is perfectly on the remove view. + } + + return true; + case MotionEvent.ACTION_UP: + if (isOverRemoveView) { + removeOverlayView(v, true); + // Not sure if setting to null is the best way to handle this. Though, + // currently it's needed to prevent a `IllegalArgumentException ... not attached to window manager` +// v = null; +// destroy(); + } else { + if (mOnClickListener != null && Math.abs(initialTouchY - event.getRawY()) <= touchSlop) { + mOnClickListener.onClick(v); + } + } + + mRemoveView.setVisibility(View.GONE); + return true; + case MotionEvent.ACTION_CANCEL: + mRemoveView.setVisibility(View.GONE); + return true; + } + return false; + } + }; + } + + /** Return true if point (x1,y1) is in the square defined by (x2,y2) with radius, otherwise false. */ + private boolean isPointInArea(int x1, int y1, int x2, int y2, int radius) { +// log("isPointInArea(). x1=" + x1 + ",y1=" + y1); +// log("isPointInArea(). x2=" + x2 + ",y2=" + y2 + ",radius=" + radius); + return x1 >= x2 - radius && x1 <= x2 + radius && y1 >= y2 - radius && y1 <= y2 + radius; + } + + /** Returns the default layout params for the overlay views. */ + private static WindowManager.LayoutParams newWindowManagerLayoutParams() { + WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_PHONE, +// WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.START; + return params; + } + + private static WindowManager.LayoutParams newWindowManagerLayoutParamsForRemoveView() { + WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_PHONE, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, + PixelFormat.TRANSLUCENT); + params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; + params.y = 56; + return params; + } + + /** Interface definition for when an overlay view has been removed. */ + public static interface OnRemoveOverlayListener { + /** This overlay has been removed. + * @param v the removed view + * @param isRemovedByUser true if user manually removed view, false if removed another way */ + public void onRemoveOverlay(View v, boolean isRemovedByUser); + } + +}