jawsware international - mobile app development
logo

856.359.4685

mobile@jawsware.com


SOURCE CODE SHARING


Interactive Overlay

DOWNLOAD SOURCE
Updated: 12/2/2011

This project shows how to create a view that is overlaid on top of all other elements on the screen. It also demonstrates how to allow the user to interact with the view.

To create an overlay view, when setting up the LayoutParams you need to set the type to TYPE_SYSTEM_OVERLAY and use the flag FLAG_WATCH_OUTSIDE_TOUCH. This presents a problem because as the Android documentation states: "you will not receive the full down/move/up gesture, only the location of the first down as an ACTION_OUTSIDE." In order to receive the full array of touch events you need to use the TYPE_SYSTEM_ALERT type, but this causes the overlay to take over the screen and stop interaction with other elements.

The solution is to use both TYPE_SYSTEM_OVERLAY and TYPE_SYSTEM_ALERT and switch between them by changing the type of the LayoutParams as needed.

This is accomplished by:

1.) Watch for the ACTION_OUTSIDE motion event.
2.) When it occurs, test if it occured within the overlay.
3.) If it did, switch the LayoutParams type to TYPE_SYSTEM_ALERT
4.) Once the interaction with the overlay is complete, switch back to TYPE_SYSTEM_OVERLAY
5.) Repeat

The one thing to keep in mind is that the ACTION_OUTSIDE motion event is always passed on to the rest of the elements on the screen. So, for example, if the overlay is on top of a button, that button will also receive the motion event and there is no way to stop it.

Also make sure you add the SYSTEM_ALERT_WINDOW permission to the mainifest file.

This sample works by extending the two abstract classes as shown below.

OverlayView.java

package com.jawsware.core.share;

/*
Copyright 2011 jawsware international

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.
*/

import android.content.Context;
import android.graphics.PixelFormat;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.RelativeLayout;

public abstract class OverlayView extends RelativeLayout {

	protected WindowManager.LayoutParams layoutParams;
	protected int layoutResId;
	protected int notificationId = 0;

	private boolean inside = false;

	public OverlayView(OverlayService service, int layoutResId, int notificationId) {
		super(service);

		this.layoutResId = layoutResId;
		this.notificationId = notificationId;

		load();
	}

	public OverlayService getService() {
		return (OverlayService) getContext();
	}

	public int getGravity() {
		// Override this to set a custom Gravity for the view.

		return Gravity.CENTER;
	}

	protected void setupLayoutParams() {
		// Override this to modify the initial LayoutParams. Be sure to call
		// super.setupLayoutParams() first.

		layoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT,
				WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
				PixelFormat.TRANSLUCENT);

		layoutParams.gravity = getGravity();
				
	}

	protected void inflateView() {
		// Inflates the layout resource, sets up the LayoutParams and adds the
		// View to the WindowManager service.

		LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);

		inflater.inflate(layoutResId, this);

		onInflateView();
		
	}

	protected void onInflateView() {
		// Override this to make calls to findViewById() to setup references to
		// the views that were inflated.
		// This is called automatically when the object is created right after
		// the resource is inflated.
	}

	public boolean isVisible() {
		// Override this method to control when the Overlay is visible without
		// destroying it.
		return true;
	}

	protected void reloadLayout() {
		unload();
		load();
	}
	
	public void refreshLayout() {
		// Call this to force the updating of the view's layout.

		removeAllViews();
		inflateView();

		refresh();
	}
	
	protected void addView() {
		setupLayoutParams();

		((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).addView(this, layoutParams);
	}

	protected void load() {
		inflateView();
		addView();
		refresh();
	}

	protected void unload() {
		((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).removeView(this);

		removeAllViews();
	}

	public void destory() {
		((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).removeView(this);
	}

	public void makeActive() {
		// Call this make the overlay active and start receiving all touch events
		
		layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;

		((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).updateViewLayout(this, layoutParams);
	}
	
	public void makeInactive() {
		layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;

		WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);

		wm.updateViewLayout(this, layoutParams);
	}

	public void refresh() {
		// Call this to update the contents of the Overlay.

		if (!isVisible()) {
			setVisibility(View.GONE);
		} else {
			setVisibility(View.VISIBLE);

			refreshViews();
		}
	}

	protected void refreshViews() {
		// Override this method to refresh the views inside of the Overlay. Only
		// called when Overlay is visible.
	}
	
	protected boolean showNotificationHidden() {
		// Override this to configure the notificaiton to remain even when the overlay is invisible.
		return true;
	}

	@Override
	public void setVisibility(int visibility) {
		//Catch changes to the Overlay's visibility in order to move the service to the foreground.
		
		super.setVisibility(visibility);

		if (visibility == View.VISIBLE) {
			getService().moveToForeground(notificationId, ! showNotificationHidden());
		} else {
			getService().moveToBackground(notificationId, ! showNotificationHidden());
		}
		
	}

	protected int getLeftOnScreen() {
		int[] location = new int[2];

		getLocationOnScreen(location);

		return location[0];
	}

	protected int getTopOnScreen() {
		int[] location = new int[2];

		getLocationOnScreen(location);

		return location[1];
	}

	
	protected boolean isInside(View view, int x, int y) {
		// Use this to test if the X, Y coordinates of the MotionEvent are
		// inside of the View specified.

		int[] location = new int[2];

		view.getLocationOnScreen(location);

		if (x >= location[0]) {
			if (x <= location[0] + view.getWidth()) {
				if (y >= location[1]) {
					if (y <= location[1] + view.getHeight()) {
						return true;
					}
				}
			}
		}

		return false;
	}

	protected void onTouchEvent_Up(MotionEvent event, boolean inside) {

	}

	protected void onTouchEvent_Move(MotionEvent event, boolean inside) {

	}

	protected void onTouchEvent_PressInactive(MotionEvent event) {

	}

	protected void onTouchEvent_PressActive(MotionEvent event, boolean inside) {

	}

	
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {

		if (event.getActionMasked() == MotionEvent.ACTION_OUTSIDE && isInside(this, (int) event.getRawX(), (int) event.getRawY())) {

			onTouchEvent_PressInactive(event);
			
		} else if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {

			if (isInside(this, (int) event.getRawX(), (int) event.getRawY())) {
				inside = true;
			} else {
				inside = false;
			}

			onTouchEvent_PressActive(event, inside);

		} else if (event.getActionMasked() == MotionEvent.ACTION_UP) {

			onTouchEvent_Up(event, inside);

		} else if (event.getActionMasked() == MotionEvent.ACTION_MOVE) {

			onTouchEvent_Move(event, inside);

		}

		return super.onTouchEvent(event);

	}
}

OverlayService.java

package com.jawsware.core.share;

/*
Copyright 2011 jawsware international

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.
*/

import android.app.Notification;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;

public class OverlayService extends Service {

	protected boolean foreground = false;
	protected boolean cancelNotification = false;
	
	protected Notification foregroundNotification(int notificationId) {
		return null;
	}

	public void moveToForeground(int id, boolean cancelNotification) {
		moveToForeground(id, foregroundNotification(id), cancelNotification);
	}

	public void moveToForeground(int id, Notification notification, boolean cancelNotification) {
		if (! this.foreground) {
			this.foreground = true;
			this.cancelNotification = cancelNotification;
					
			super.startForeground(id, notification);
		}
	}
	

	public void moveToBackground(int id, boolean cancelNotification) {
		foreground = false;
		
		super.stopForeground(cancelNotification);
	}
	
	public void moveToBackground(int id) {
		moveToBackground(id, cancelNotification);
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		return START_STICKY;
	}
	
	@Override
	public IBinder onBind(Intent intent) {
		return null;
	}

}

SampleOverlayView.java

package samples.jawsware.interactiveoverlay;

/*
Copyright 2011 jawsware international

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.
*/

import android.view.Gravity;
import android.view.MotionEvent;
import android.widget.TextView;

import com.jawsware.core.share.OverlayService;
import com.jawsware.core.share.OverlayView;

public class SampleOverlayView extends OverlayView {

	private TextView info;
	
	public SampleOverlayView(OverlayService service) {
		super(service, R.layout.overlay, 1);
	}

	public int getGravity() {
		return Gravity.TOP + Gravity.RIGHT;
	}
	
	@Override
	protected void onInflateView() {
		info = (TextView) this.findViewById(R.id.textview_info);
	}

	@Override
	protected void refreshViews() {
		info.setText("INACTIVE\nWAITING\nWAITING");
	}

	@Override
	protected void onTouchEvent_Up(MotionEvent event, boolean inside) {
		if (inside) {
			makeInactive();
			info.setText("INACTIVE\nPOINTERS: " + event.getPointerCount());
		} else {
			info.setText("UP\nOUTSIDE\nPOINTERS: " + event.getPointerCount());
		}
	}

	@Override
	protected void onTouchEvent_Move(MotionEvent event, boolean inside) {
		if (inside) {
			info.setText("MOVE\nINSIDE\nPOINTERS: " + event.getPointerCount());
		} else {
			info.setText("MOVE\nOUTSIDE\nPOINTERS: " + event.getPointerCount());
		}
	}

	@Override
	protected void onTouchEvent_PressInactive(MotionEvent event) {
		makeActive();
		info.setText("ACTIVE\nPOINTERS: " + event.getPointerCount());
	}

	@Override
	protected void onTouchEvent_PressActive(MotionEvent event, boolean inside) {
		if (inside) {
			info.setText("DOWN\nINSIDE\nPOINTERS: " + event.getPointerCount());
		} else {
			info.setText("DOWN\nOUTSIDE\nPOINTERS: " + event.getPointerCount());
		}
	}
	
	
	
}

SampleOverlayService.java

package samples.jawsware.interactiveoverlay;

/*
Copyright 2011 jawsware international

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.
*/

import com.jawsware.core.share.OverlayService;

import android.app.Notification;
import android.app.PendingIntent;

import android.content.Intent;

public class SampleOverlayService extends OverlayService {

	public static SampleOverlayService instance;

	private SampleOverlayView overlayView;

	@Override
	public void onCreate() {
		super.onCreate();
		
		instance = this;
		
		overlayView = new SampleOverlayView(this);
	}

	@Override
	public void onDestroy() {
		super.onDestroy();

		if (overlayView != null) {
			overlayView.destory();
		}

	}
	
	static public void stop() {
		if (instance != null) {
			instance.stopSelf();
		}
	}
	
	@Override
	protected Notification foregroundNotification(int notificationId) {
		Notification notification;

		notification = new Notification(R.drawable.ic_launcher, getString(R.string.title_notification), System.currentTimeMillis());

		notification.flags = notification.flags | Notification.FLAG_ONGOING_EVENT | Notification.FLAG_ONLY_ALERT_ONCE;

		notification.setLatestEventInfo(this, getString(R.string.title_notification), getString(R.string.message_notification), notificationIntent());

		return notification;
	}


	private PendingIntent notificationIntent() {
		Intent intent = new Intent(this, SampleOverlayHideActivity.class);

		PendingIntent pending = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

		return pending;
	}

}

jawsware international856.359.4685mobile@jawsware.com
AppsCode