Reputation: 770
I want my my app to display a floating bubble notification as in facebook messenger. Following https://www.androidhive.info/2016/11/android-floating-widget-like-facebook-chat-head/ below is the service I wrote.
import android.annotation.SuppressLint;
import android.app.Service;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.IBinder;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
public class BubbleNotifyService extends Service {
private WindowManager windowManager;
private View BubbleView;
private TextView bubbleTitle, bubbleData;
public BubbleNotifyService() {
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@SuppressLint({"RtlHardcoded", "InflateParams"})
@Override
public void onCreate() {
super.onCreate();
//android.os.Debug.waitForDebugger();
setTheme(R.style.AppTheme);
BubbleView =
LayoutInflater.from(this).inflate(R.layout.floating_bubble, null);
final WindowManager.LayoutParams params;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
);
} else {
params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE ,
PixelFormat.TRANSLUCENT
);
}
if (BubbleView == null) {
Log.d("Bubble", "BubbleView is null.");
}
params.gravity = Gravity.TOP | Gravity.LEFT;
params.x = 0;
params.y = 100;
windowManager =
(WindowManager) getSystemService(WINDOW_SERVICE);
if (windowManager != null) {
Log.d("Bubble", "added bubble to view.");
windowManager.addView(BubbleView, params);
} else {
Log.d("Bubble", "windowManager is null");
}
final View collapsedView =
BubbleView.findViewById(R.id.collapsed_view);
final View expandedView =
BubbleView.findViewById(R.id.expanded_view);
expandedView.setVisibility(View.GONE);
collapsedView.setVisibility(View.VISIBLE);
ImageView close_collapsed = BubbleView.findViewById(R.id.collaspsed_cancel);
ImageView close_expanded = BubbleView.findViewById(R.id.expanded_cancel);
Button open_act_btn = BubbleView.findViewById(R.id.open_full_btn);
bubbleTitle = BubbleView.findViewById(R.id.bubble_title);
bubbleMeaning = BubbleView.findViewById(R.id.bubble_data);
close_collapsed.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
stopSelf();
}
});
close_expanded.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
collapsedView.setVisibility(View.VISIBLE);
expandedView.setVisibility(View.GONE);
}
});
open_act_btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//TODO new Activity
}
});
BubbleView.findViewById(R.id.bubble_root).setOnTouchListener(new View.OnTouchListener() {
private int initialX;
private int initialY;
private float initialTouchX;
private float initialTouchY;
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
initialX = params.x;
initialY = params.y;
initialTouchX = motionEvent.getRawX();
initialTouchY = motionEvent.getRawY();
return true;
case MotionEvent.ACTION_UP:
int Xdiff = (int) (motionEvent.getRawX() - initialTouchX);
int Ydiff = (int) (motionEvent.getRawY() - initialTouchY);
if (Xdiff < 10 && Ydiff < 10) {
if (isViewCollapsed()) {
collapsedView.setVisibility(View.GONE);
expandedView.setVisibility(View.VISIBLE);
}
}
return true;
case MotionEvent.ACTION_MOVE:
params.x =
initialX + (int) (motionEvent.getRawX() - initialTouchX);
params.y =
initialY + (int) (motionEvent.getRawY() - initialTouchY);
windowManager.updateViewLayout(BubbleView, params);
return true;
}
return false;
}
});
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
String title = intent.getStringExtra(IntentKeys.BUBBLE_DATA_TITLE);
String data = intent.getStringExtra(IntentKeys.BUBBLE_DATA);
bubbleTitle.setText(title);
bubbleData.setText(data);
return START_NOT_STICKY;
}
private boolean isViewCollapsed() {
return BubbleView == null ||
BubbleView.findViewById(R.id.collapsed_view).getVisibility() == View.VISIBLE;
}
@Override
public void onDestroy() {
super.onDestroy();
if (BubbleView != null) {
windowManager.removeView(BubbleView);
}
}
}
BubbleNotifyService
is called from another service that receives notification as:
Intent intent = new Intent(context, BubbleNotifyService.class);
intent.putExtra(IntentKeys.BUBBLE_DATA_TITLE, Results.getWord());
intent.putExtra(IntentKeys.BUBBLE_DATA, Results.getData());
context.startService(intent);
All data from the intent is passed to BubbleNotifyService
. The service is running as a separate process as specified in manifest android:process=":BubbleResult"
, But the service does not display any overlay. Draw over other app permission is granted to the app.
Layout for bubble(floating_bubble.xml)
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/bubble_frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<android.support.constraint.ConstraintLayout
android:id="@+id/bubble_root"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<android.support.constraint.ConstraintLayout
android:id="@+id/collapsed_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible">
<ImageView
android:id="@+id/collapsed_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@mipmap/ic_launcher"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/collaspsed_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@+id/collapsed_icon"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_action_cancel"
tools:ignore="ContentDescription" />
</android.support.constraint.ConstraintLayout>
<android.support.v7.widget.CardView
android:id="@+id/expanded_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
app:cardCornerRadius="5dp"
app:cardElevation="10dp"
app:cardUseCompatPadding="true"
app:contentPadding="5dp">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/bubble_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/expanded_cancel"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/result_card_title" />
<TextView
android:id="@+id/bubble_data"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bubble_title"
tools:text="@string/result_card_data" />
<ImageView
android:id="@+id/expanded_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bubble_title"
app:srcCompat="@drawable/ic_action_cancel"
tools:ignore="ContentDescription" />
<Button
android:id="@+id/open_full_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:text="@string/result_card_see_more"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bubble_data" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
</android.support.constraint.ConstraintLayout>
</FrameLayout>
Upvotes: 8
Views: 9768
Reputation: 1108
Tested both Your code and mentioned blog post's code. The problem seems to be because You have used ConstraintLayout
and CardView
in Your layout.
For widget behavior, these view types are not supported. When we add view directly to the system window they behave likes RemoteViews
. As soon as You change ConstraintLayout
and CardView
, Your layout becomes visible and operable.
Upvotes: 6
Reputation: 58974
Here is my working code. You will start this Service when you need to show floating view.
FloatingService.class
import android.app.Service;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.Build;
import android.os.IBinder;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.Toast;
public class FloatingService extends Service {
private WindowManager mWindowManager;
private View mFloatingView;
public FloatingService() {
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
//Inflate the floating view layout we created
mFloatingView = LayoutInflater.from(this).inflate(R.layout.layout_floating_widget, null);
int LAYOUT_FLAG;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
LAYOUT_FLAG = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
LAYOUT_FLAG = WindowManager.LayoutParams.TYPE_PHONE;
}
//Add the view to the window.
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
LAYOUT_FLAG,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
//Specify the view position
params.gravity = Gravity.TOP | Gravity.LEFT; //Initially view will be added to top-left corner
params.x = 0;
params.y = 100;
//Add the view to the window
mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
mWindowManager.addView(mFloatingView, params);
//The root element of the collapsed view layout
final View collapsedView = mFloatingView.findViewById(R.id.collapse_view);
//The root element of the expanded view layout
final View expandedView = mFloatingView.findViewById(R.id.expanded_container);
//Set the close button
ImageView closeButtonCollapsed = (ImageView) mFloatingView.findViewById(R.id.close_btn);
closeButtonCollapsed.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//close the service and remove the from from the window
stopSelf();
}
});
//Set the view while floating view is expanded.
//Set the play button.
ImageView playButton = (ImageView) mFloatingView.findViewById(R.id.play_btn);
playButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(FloatingService.this, "Playing the song.", Toast.LENGTH_LONG).show();
}
});
//Set the next button.
ImageView nextButton = (ImageView) mFloatingView.findViewById(R.id.next_btn);
nextButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(FloatingService.this, "Playing next song.", Toast.LENGTH_LONG).show();
}
});
//Set the pause button.
ImageView prevButton = (ImageView) mFloatingView.findViewById(R.id.prev_btn);
prevButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(FloatingService.this, "Playing previous song.", Toast.LENGTH_LONG).show();
}
});
//Set the close button
ImageView closeButton = (ImageView) mFloatingView.findViewById(R.id.close_button);
closeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
collapsedView.setVisibility(View.VISIBLE);
expandedView.setVisibility(View.GONE);
}
});
//Open the application on thi button click
ImageView openButton = (ImageView) mFloatingView.findViewById(R.id.open_button);
openButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//Open the application click.
Intent intent = new Intent(FloatingService.this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
//close the service and remove view from the view hierarchy
stopSelf();
}
});
//Drag and move floating view using user's touch action.
mFloatingView.findViewById(R.id.root_container).setOnTouchListener(new View.OnTouchListener() {
private int initialX;
private int initialY;
private float initialTouchX;
private float initialTouchY;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//remember the initial position.
initialX = params.x;
initialY = params.y;
//get the touch location
initialTouchX = event.getRawX();
initialTouchY = event.getRawY();
return true;
case MotionEvent.ACTION_UP:
int Xdiff = (int) (event.getRawX() - initialTouchX);
int Ydiff = (int) (event.getRawY() - initialTouchY);
//The check for Xdiff <10 && YDiff< 10 because sometime elements moves a little while clicking.
//So that is click event.
if (Xdiff < 10 && Ydiff < 10) {
if (isViewCollapsed()) {
//When user clicks on the image view of the collapsed layout,
//visibility of the collapsed layout will be changed to "View.GONE"
//and expanded view will become visible.
collapsedView.setVisibility(View.GONE);
expandedView.setVisibility(View.VISIBLE);
}
}
return true;
case MotionEvent.ACTION_MOVE:
//Calculate the X and Y coordinates of the view.
params.x = initialX + (int) (event.getRawX() - initialTouchX);
params.y = initialY + (int) (event.getRawY() - initialTouchY);
//Update the layout with new X & Y coordinate
mWindowManager.updateViewLayout(mFloatingView, params);
return true;
}
return false;
}
});
}
/**
* Detect if the floating view is collapsed or expanded.
*
* @return true if the floating view is collapsed.
*/
private boolean isViewCollapsed() {
return mFloatingView == null || mFloatingView.findViewById(R.id.collapse_view).getVisibility() == View.VISIBLE;
}
@Override
public void onDestroy() {
super.onDestroy();
if (mFloatingView != null) mWindowManager.removeView(mFloatingView);
}
}
Then add
<service
android:name="yourpakcage.FloatingService"
android:enabled="true"
android:exported="false"/>
layout_floating_widget.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<!--Root container-->
<RelativeLayout
android:id="@+id/root_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:ignore="UselessParent">
<!--View while view is collapsed-->
<RelativeLayout
android:id="@+id/collapse_view"
android:layout_width="wrap_content"
android:visibility="visible"
android:layout_height="wrap_content"
android:orientation="vertical">
<!--Icon of floating widget -->
<ImageView
android:id="@+id/collapsed_iv"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginTop="8dp"
android:src="@drawable/ic_android_circle"
tools:ignore="ContentDescription"/>
<!--Close button-->
<ImageView
android:id="@+id/close_btn"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginLeft="40dp"
android:src="@drawable/ic_close"
tools:ignore="ContentDescription"/>
</RelativeLayout>
<!--View while view is expanded-->
<LinearLayout
android:id="@+id/expanded_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#F8BBD0"
android:visibility="gone"
android:orientation="horizontal"
android:padding="8dp">
<!--Album image for the song currently playing.-->
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/music_player"
tools:ignore="ContentDescription"/>
<!--Previous button-->
<ImageView
android:id="@+id/prev_btn"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="20dp"
android:src="@mipmap/ic_previous"
tools:ignore="ContentDescription"/>
<!--Play button-->
<ImageView
android:id="@+id/play_btn"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"
android:src="@mipmap/ic_play"
tools:ignore="ContentDescription"/>
<!--Next button-->
<ImageView
android:id="@+id/next_btn"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"
android:src="@mipmap/ic_play_next"
tools:ignore="ContentDescription"/>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/close_button"
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/ic_close"/>
<ImageView
android:id="@+id/open_button"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_alignParentBottom="true"
android:src="@drawable/ic_open"/>
</RelativeLayout>
</LinearLayout>
</RelativeLayout>
</FrameLayout>
Please add dummy drawables and strings.
To show your floating view.
startService(new Intent(passContext, FloatingViewService.class));
Upvotes: 3
Reputation: 816
Unfortunately the guide you've been following is outdated. You're trying to start a service while your app is in the background which results in an IllegalArgumentException
because of the Background Execution Limits introduced in Android Oreo. A migration guide is included at the bottom of the link.
Upvotes: 1