Floating widgets are the views that float over the screen. We all love the chat heads (or chat bubbles) from the popular Facebook Messenger. This provides very handy and easy access to the chat conversation screen no matter on which screen you are. Chat heads are very convenient for multitasking as a user can work and chat at the same time. That means if you are using the any app on your phone and if any new message arrives, you can open the conversation and replay the message without switching app. Pretty cool!!!
In this tutorial, we are going to learn how to create simple chat head and allow user to drag them across the screen. So that user can adjust the position of the floating widget in the screen.
1. Create a new project in Android Studio by navigating to File ⇒ New ⇒ New Project and fill required details. By default my activity is MainActivity.java.
2. Open res ⇒ values ⇒ strings.xml and add below string values. These are some strings that we are going to use in our project.
1 2 3 4 5 6 7 |
<resources> <string name="app_name">Floating Widget Demo</string> <string name="create_floating_widget">Create Floating Widget</string> <string name="floating_widget_label">Floating Widget Label</string> <string name="floating_widget_details">Floating Widget Details</string> <string name="draw_other_app_permission_denied">Draw over other app permission not available. App won\'t work without permission. Please try again.</string> </resources> |
3. Add android.permission.SYSTEM_ALERT_WINDOW permission to the AndroidManifest.xml file. This permission allows an app to create windows, shown on top of all other apps.
1 2 |
<!-- Permission required to draw floating widget over other apps --> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> |
4. Create a new layout naming floating_widget_layout.xml for the floating widget view. This layout will contain two main views.
Collapsed view:
The floating widget will remain collapsed when the view is launched. When the user clicks on this view, an expanded view will open.
Expanded View:
This view will contain all the views that you want to display when user clicks on Floating Widget.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
<?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 of Floating Widget View --> <RelativeLayout android:id="+id/root_container" android:layout_width="wrap_content" android:layout_height="wrap_content"> <!-- View while view is collapsed --> <RelativeLayout android:id="+id/collapse_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:visibility="visible"> <!-- ImageView of floating widget --> <ImageView android:id="+id/collapsed_iv" android:layout_width="70dp" android:layout_height="70dp" android:layout_marginTop="8dp" android:src="mipmap/ic_launcher_round" tools:ignore="ContentDescription" /> <!-- Close button to close Floating Widget View --> <ImageView android:id="+id/close_floating_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="50dp" android:layout_marginTop="5dp" android:background="drawable/circle_shape" android:src="drawable/ic_close_white_24dp" 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="android:color/white" android:gravity="center" android:orientation="horizontal" android:padding="8dp" android:visibility="gone"> <ImageView android:id="+id/floating_widget_image_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="mipmap/ic_launcher" /> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dp" android:orientation="vertical"> <TextView android:id="+id/floating_widget_title_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_vertical" android:text="string/floating_widget_label" android:textColor="android:color/black" android:textSize="14sp" /> <TextView android:id="+id/floating_widget_detail_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_vertical" android:text="string/floating_widget_details" android:textColor="android:color/darker_gray" android:textSize="11sp" /> </LinearLayout> <!-- ImageView to Close Expanded View --> <ImageView android:id="+id/close_expanded_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:padding="10dp" android:src="drawable/ic_close_black_24dp" /> <!-- ImageView to Open Activity --> <ImageView android:id="+id/open_activity_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:padding="10dp" android:src="drawable/ic_aspect_ratio_black_24dp" /> </LinearLayout> </RelativeLayout> </FrameLayout> |
5. For chat head if you long press it and drag to bottom a remove layout shows with close image. So we have to create the same layout naming remove_floating_widget_layout.xml and add the ImageView with cross image to it. This view will be displayed when user hold/long press the chat head.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="+id/remove_relativelayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp"> <ImageView android:id="+id/remove_img" android:layout_width="60dp" android:layout_height="60dp" android:background="drawable/white_circle_shape" android:padding="10dp" android:src="drawable/ic_close_white_24dp" /> </RelativeLayout> |
6. Below are the custom drawables and vector drawables that i am using in this project.
1 2 3 4 5 6 |
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="android:color/black" /> </shape> |
1 2 3 4 5 6 7 8 9 |
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0"> <path android:fillColor="#FF000000" android:pathData="M19,12h-2v3h-3v2h5v-5zM7,9h3L10,7L5,7v5h2L7,9zM21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19.01L3,19.01L3,4.99h18v14.02z"/> </vector> |
1 2 3 4 5 6 7 8 9 |
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0"> <path android:fillColor="#FF000000" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/> </vector> |
1 2 3 4 5 6 7 8 9 |
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0"> <path android:fillColor="#FFFFFFFF" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" /> </vector> |
1 2 3 4 5 6 7 8 9 |
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <stroke android:width="1dp" android:color="android:color/white" /> </shape> |
7. Now create a service class called FloatingWidgetService.java. Whenever you want to display a chat head, start the service using startService() command. In onCreate() of the service we will add the layout of the chat head at the top-left corner of the window.
In this class we are adding both Floating Widget View and Remove View to Window manager and initially hiding Remove View and showing only Collapse View of Floating Widget.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
public class FloatingWidgetService extends Service implements View.OnClickListener { private WindowManager mWindowManager; private View mFloatingWidgetView, collapsedView, expandedView; private ImageView remove_image_view; private View removeFloatingWidgetView; public FloatingWidgetService() { } @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); //init WindowManager mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE); //Init LayoutInflater LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); addRemoveView(inflater); addFloatingWidgetView(inflater); } /* Add Remove View to Window Manager */ private View addRemoveView(LayoutInflater inflater) { //Inflate the removing view layout we created removeFloatingWidgetView = inflater.inflate(R.layout.remove_floating_widget_layout, null); //Add the view to the window. WindowManager.LayoutParams paramRemove = 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); //Specify the view position paramRemove.gravity = Gravity.TOP | Gravity.LEFT; //Initially the Removing widget view is not visible, so set visibility to GONE removeFloatingWidgetView.setVisibility(View.GONE); remove_image_view = (ImageView) removeFloatingWidgetView.findViewById(R.id.remove_img); //Add the view to the window mWindowManager.addView(removeFloatingWidgetView, paramRemove); return remove_image_view; } /* Add Floating Widget View to Window Manager */ private void addFloatingWidgetView(LayoutInflater inflater) { //Inflate the floating view layout we created mFloatingWidgetView = inflater.inflate(R.layout.floating_widget_layout, null); //Add the view to the window. final WindowManager.LayoutParams params = new WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_PHONE, 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, you change x-y coordinates according to your need params.x = 0; params.y = 100; //Add the view to the window mWindowManager.addView(mFloatingWidgetView, params); //find id of collapsed view layout collapsedView = mFloatingWidgetView.findViewById(R.id.collapse_view); //find id of the expanded view layout expandedView = mFloatingWidgetView.findViewById(R.id.expanded_container); } @Override public void onDestroy() { super.onDestroy(); /* on destroy remove both view from window manager */ if (mFloatingWidgetView != null) mWindowManager.removeView(mFloatingWidgetView); if (removeFloatingWidgetView != null) mWindowManager.removeView(removeFloatingWidgetView); } |
8. To drag the chat head along with the user’s touch, we have to override OnTouchListener(). Whenever the user touches the chat head, we will record the initial x and y coordinates, and when the user moves the finger, the application will calculate the new X and Y coordinate and move the chat head.
The below code is performing various process:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
//Drag and move floating view using user's touch action. mFloatingWidgetView.findViewById(R.id.root_container).setOnTouchListener(new View.OnTouchListener() { long time_start = 0, time_end = 0; boolean isLongClick = false;//variable to judge if user click long press boolean inBounded = false;//variable to judge if floating view is bounded to remove view int remove_img_width = 0, remove_img_height = 0; Handler handler_longClick = new Handler(); Runnable runnable_longClick = new Runnable() { @Override public void run() { //On Floating Widget Long Click //Set isLongClick as true isLongClick = true; //Set remove widget view visibility to VISIBLE removeFloatingWidgetView.setVisibility(View.VISIBLE); onFloatingWidgetLongClick(); } }; @Override public boolean onTouch(View v, MotionEvent event) { //Get Floating widget view params WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) mFloatingWidgetView.getLayoutParams(); //get the touch location coordinates int x_cord = (int) event.getRawX(); int y_cord = (int) event.getRawY(); int x_cord_Destination, y_cord_Destination; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: time_start = System.currentTimeMillis(); handler_longClick.postDelayed(runnable_longClick, 600); remove_img_width = remove_image_view.getLayoutParams().width; remove_img_height = remove_image_view.getLayoutParams().height; x_init_cord = x_cord; y_init_cord = y_cord; //remember the initial position. x_init_margin = layoutParams.x; y_init_margin = layoutParams.y; return true; case MotionEvent.ACTION_UP: isLongClick = false; removeFloatingWidgetView.setVisibility(View.GONE); remove_image_view.getLayoutParams().height = remove_img_height; remove_image_view.getLayoutParams().width = remove_img_width; handler_longClick.removeCallbacks(runnable_longClick); //If user drag and drop the floating widget view into remove view then stop the service if (inBounded) { stopSelf(); inBounded = false; break; } //Get the difference between initial coordinate and current coordinate int x_diff = x_cord - x_init_cord; int y_diff = y_cord - y_init_cord; //The check for x_diff <5 && y_diff< 5 because sometime elements moves a little while clicking. //So that is click event. if (Math.abs(x_diff) < 5 && Math.abs(y_diff) < 5) { time_end = System.currentTimeMillis(); //Also check the difference between start time and end time should be less than 300ms if ((time_end - time_start) < 300) onFloatingWidgetClick(); } y_cord_Destination = y_init_margin + y_diff; int barHeight = getStatusBarHeight(); if (y_cord_Destination < 0) { y_cord_Destination = 0; } else if (y_cord_Destination + (mFloatingWidgetView.getHeight() + barHeight) > szWindow.y) { y_cord_Destination = szWindow.y - (mFloatingWidgetView.getHeight() + barHeight); } layoutParams.y = y_cord_Destination; inBounded = false; //reset position if user drags the floating view resetPosition(x_cord); return true; case MotionEvent.ACTION_MOVE: int x_diff_move = x_cord - x_init_cord; int y_diff_move = y_cord - y_init_cord; x_cord_Destination = x_init_margin + x_diff_move; y_cord_Destination = y_init_margin + y_diff_move; //If user long click the floating view, update remove view if (isLongClick) { int x_bound_left = szWindow.x / 2 - (int) (remove_img_width * 1.5); int x_bound_right = szWindow.x / 2 + (int) (remove_img_width * 1.5); int y_bound_top = szWindow.y - (int) (remove_img_height * 1.5); //If Floating view comes under Remove View update Window Manager if ((x_cord >= x_bound_left && x_cord <= x_bound_right) && y_cord >= y_bound_top) { inBounded = true; int x_cord_remove = (int) ((szWindow.x - (remove_img_height * 1.5)) / 2); int y_cord_remove = (int) (szWindow.y - ((remove_img_width * 1.5) + getStatusBarHeight())); if (remove_image_view.getLayoutParams().height == remove_img_height) { remove_image_view.getLayoutParams().height = (int) (remove_img_height * 1.5); remove_image_view.getLayoutParams().width = (int) (remove_img_width * 1.5); WindowManager.LayoutParams param_remove = (WindowManager.LayoutParams) removeFloatingWidgetView.getLayoutParams(); param_remove.x = x_cord_remove; param_remove.y = y_cord_remove; mWindowManager.updateViewLayout(removeFloatingWidgetView, param_remove); } layoutParams.x = x_cord_remove + (Math.abs(removeFloatingWidgetView.getWidth() - mFloatingWidgetView.getWidth())) / 2; layoutParams.y = y_cord_remove + (Math.abs(removeFloatingWidgetView.getHeight() - mFloatingWidgetView.getHeight())) / 2; //Update the layout with new X & Y coordinate mWindowManager.updateViewLayout(mFloatingWidgetView, layoutParams); break; } else { //If Floating window gets out of the Remove view update Remove view again inBounded = false; remove_image_view.getLayoutParams().height = remove_img_height; remove_image_view.getLayoutParams().width = remove_img_width; onFloatingWidgetClick(); } } layoutParams.x = x_cord_Destination; layoutParams.y = y_cord_Destination; //Update the layout with new X & Y coordinate mWindowManager.updateViewLayout(mFloatingWidgetView, layoutParams); return true; } return false; } }); |
Method responsible for updating Remove View Window on Floating widget long click.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/* on Floating Widget Long Click, increase the size of remove view as it look like taking focus */ private void onFloatingWidgetLongClick() { //Get remove Floating view params WindowManager.LayoutParams removeParams = (WindowManager.LayoutParams) removeFloatingWidgetView.getLayoutParams(); //get x and y coordinates of remove view int x_cord = (szWindow.x - removeFloatingWidgetView.getWidth()) / 2; int y_cord = szWindow.y - (removeFloatingWidgetView.getHeight() + getStatusBarHeight()); removeParams.x = x_cord; removeParams.y = y_cord; //Update Remove view params mWindowManager.updateViewLayout(removeFloatingWidgetView, removeParams); } |
This method will reset the position when user drag and place the Floating View to another place. This method help to prevent the Floating View not to be placed in centre of the screen. It will always place the Floating View to Left or Right of the screen depending on user dragging.
1 2 3 4 5 6 7 8 9 10 11 |
/* Reset position of Floating Widget view on dragging */ private void resetPosition(int x_cord_now) { if (x_cord_now <= szWindow.x / 2) { isLeft = true; moveToLeft(x_cord_now); } else { isLeft = false; moveToRight(x_cord_now); } } |
This method will move the Floating View to Left of the Screen when user drags.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/* Method to move the Floating widget view to Left */ private void moveToLeft(final int current_x_cord) { final int x = szWindow.x - current_x_cord; new CountDownTimer(500, 5) { //get params of Floating Widget view WindowManager.LayoutParams mParams = (WindowManager.LayoutParams) mFloatingWidgetView.getLayoutParams(); public void onTick(long t) { long step = (500 - t) / 5; mParams.x = 0 - (int) (current_x_cord * current_x_cord * step); //If you want bounce effect uncomment below line and comment above line // mParams.x = 0 - (int) (double) bounceValue(step, x); //Update window manager for Floating Widget mWindowManager.updateViewLayout(mFloatingWidgetView, mParams); } public void onFinish() { mParams.x = 0; //Update window manager for Floating Widget mWindowManager.updateViewLayout(mFloatingWidgetView, mParams); } }.start(); } |
This method will move the Floating View to Right of the Screen when user drags.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/* Method to move the Floating widget view to Right */ private void moveToRight(final int current_x_cord) { new CountDownTimer(500, 5) { //get params of Floating Widget view WindowManager.LayoutParams mParams = (WindowManager.LayoutParams) mFloatingWidgetView.getLayoutParams(); public void onTick(long t) { long step = (500 - t) / 5; mParams.x = (int) (szWindow.x + (current_x_cord * current_x_cord * step) - mFloatingWidgetView.getWidth()); //If you want bounce effect uncomment below line and comment above line // mParams.x = szWindow.x + (int) (double) bounceValue(step, x_cord_now) - mFloatingWidgetView.getWidth(); //Update window manager for Floating Widget mWindowManager.updateViewLayout(mFloatingWidgetView, mParams); } public void onFinish() { mParams.x = szWindow.x - mFloatingWidgetView.getWidth(); //Update window manager for Floating Widget mWindowManager.updateViewLayout(mFloatingWidgetView, mParams); } }.start(); } |
This method will return the status bar height according to device display metrics.
1 2 3 4 |
/* return status bar height on basis of device display metrics */ private int getStatusBarHeight() { return (int) Math.ceil(25 * getApplicationContext().getResources().getDisplayMetrics().density); } |
When user click the Floating Widget the below method will trigger and display the Expanded View.
1 2 3 4 5 6 7 8 9 10 11 |
/* on Floating widget click show expanded view */ private void onFloatingWidgetClick() { 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); } } |
Method to check if Collapse View is visible or not.
1 2 3 4 |
/* Detect if the floating view is collapsed or expanded */ private boolean isViewCollapsed() { return mFloatingWidgetView == null || mFloatingWidgetView.findViewById(R.id.collapse_view).getVisibility() == View.VISIBLE; } |
Also, implement click listener to close the chat head by stopping the service when the user clicks on the close icon at the top-right of the chat head.
1 2 |
//close the service and remove the from from the window stopSelf(); |
9. Finally the FloatingWidgetService.java will look like below. There are some extra click events as well to close the expanded view and open the activity from expanded view.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 |
package com.floatingwidgetchathead_demo; import android.app.Service; import android.content.Intent; import android.content.res.Configuration; import android.graphics.PixelFormat; import android.graphics.Point; import android.os.Build; import android.os.CountDownTimer; import android.os.Handler; import android.os.IBinder; import android.support.annotation.Nullable; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.widget.ImageView; /** * Created by sonu on 28/03/17. */ public class FloatingWidgetService extends Service implements View.OnClickListener { private WindowManager mWindowManager; private View mFloatingWidgetView, collapsedView, expandedView; private ImageView remove_image_view; private Point szWindow = new Point(); private View removeFloatingWidgetView; private int x_init_cord, y_init_cord, x_init_margin, y_init_margin; //Variable to check if the Floating widget view is on left side or in right side // initially we are displaying Floating widget view to Left side so set it to true private boolean isLeft = true; public FloatingWidgetService() { } @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); //init WindowManager mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE); getWindowManagerDefaultDisplay(); //Init LayoutInflater LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); addRemoveView(inflater); addFloatingWidgetView(inflater); implementClickListeners(); implementTouchListenerToFloatingWidgetView(); } /* Add Remove View to Window Manager */ private View addRemoveView(LayoutInflater inflater) { //Inflate the removing view layout we created removeFloatingWidgetView = inflater.inflate(R.layout.remove_floating_widget_layout, null); //Add the view to the window. WindowManager.LayoutParams paramRemove = 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); //Specify the view position paramRemove.gravity = Gravity.TOP | Gravity.LEFT; //Initially the Removing widget view is not visible, so set visibility to GONE removeFloatingWidgetView.setVisibility(View.GONE); remove_image_view = (ImageView) removeFloatingWidgetView.findViewById(R.id.remove_img); //Add the view to the window mWindowManager.addView(removeFloatingWidgetView, paramRemove); return remove_image_view; } /* Add Floating Widget View to Window Manager */ private void addFloatingWidgetView(LayoutInflater inflater) { //Inflate the floating view layout we created mFloatingWidgetView = inflater.inflate(R.layout.floating_widget_layout, null); //Add the view to the window. final WindowManager.LayoutParams params = new WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_PHONE, 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, you change x-y coordinates according to your need params.x = 0; params.y = 100; //Add the view to the window mWindowManager.addView(mFloatingWidgetView, params); //find id of collapsed view layout collapsedView = mFloatingWidgetView.findViewById(R.id.collapse_view); //find id of the expanded view layout expandedView = mFloatingWidgetView.findViewById(R.id.expanded_container); } private void getWindowManagerDefaultDisplay() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) mWindowManager.getDefaultDisplay().getSize(szWindow); else { int w = mWindowManager.getDefaultDisplay().getWidth(); int h = mWindowManager.getDefaultDisplay().getHeight(); szWindow.set(w, h); } } /* Implement Touch Listener to Floating Widget Root View */ private void implementTouchListenerToFloatingWidgetView() { //Drag and move floating view using user's touch action. mFloatingWidgetView.findViewById(R.id.root_container).setOnTouchListener(new View.OnTouchListener() { long time_start = 0, time_end = 0; boolean isLongClick = false;//variable to judge if user click long press boolean inBounded = false;//variable to judge if floating view is bounded to remove view int remove_img_width = 0, remove_img_height = 0; Handler handler_longClick = new Handler(); Runnable runnable_longClick = new Runnable() { @Override public void run() { //On Floating Widget Long Click //Set isLongClick as true isLongClick = true; //Set remove widget view visibility to VISIBLE removeFloatingWidgetView.setVisibility(View.VISIBLE); onFloatingWidgetLongClick(); } }; @Override public boolean onTouch(View v, MotionEvent event) { //Get Floating widget view params WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) mFloatingWidgetView.getLayoutParams(); //get the touch location coordinates int x_cord = (int) event.getRawX(); int y_cord = (int) event.getRawY(); int x_cord_Destination, y_cord_Destination; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: time_start = System.currentTimeMillis(); handler_longClick.postDelayed(runnable_longClick, 600); remove_img_width = remove_image_view.getLayoutParams().width; remove_img_height = remove_image_view.getLayoutParams().height; x_init_cord = x_cord; y_init_cord = y_cord; //remember the initial position. x_init_margin = layoutParams.x; y_init_margin = layoutParams.y; return true; case MotionEvent.ACTION_UP: isLongClick = false; removeFloatingWidgetView.setVisibility(View.GONE); remove_image_view.getLayoutParams().height = remove_img_height; remove_image_view.getLayoutParams().width = remove_img_width; handler_longClick.removeCallbacks(runnable_longClick); //If user drag and drop the floating widget view into remove view then stop the service if (inBounded) { stopSelf(); inBounded = false; break; } //Get the difference between initial coordinate and current coordinate int x_diff = x_cord - x_init_cord; int y_diff = y_cord - y_init_cord; //The check for x_diff <5 && y_diff< 5 because sometime elements moves a little while clicking. //So that is click event. if (Math.abs(x_diff) < 5 && Math.abs(y_diff) < 5) { time_end = System.currentTimeMillis(); //Also check the difference between start time and end time should be less than 300ms if ((time_end - time_start) < 300) onFloatingWidgetClick(); } y_cord_Destination = y_init_margin + y_diff; int barHeight = getStatusBarHeight(); if (y_cord_Destination < 0) { y_cord_Destination = 0; } else if (y_cord_Destination + (mFloatingWidgetView.getHeight() + barHeight) > szWindow.y) { y_cord_Destination = szWindow.y - (mFloatingWidgetView.getHeight() + barHeight); } layoutParams.y = y_cord_Destination; inBounded = false; //reset position if user drags the floating view resetPosition(x_cord); return true; case MotionEvent.ACTION_MOVE: int x_diff_move = x_cord - x_init_cord; int y_diff_move = y_cord - y_init_cord; x_cord_Destination = x_init_margin + x_diff_move; y_cord_Destination = y_init_margin + y_diff_move; //If user long click the floating view, update remove view if (isLongClick) { int x_bound_left = szWindow.x / 2 - (int) (remove_img_width * 1.5); int x_bound_right = szWindow.x / 2 + (int) (remove_img_width * 1.5); int y_bound_top = szWindow.y - (int) (remove_img_height * 1.5); //If Floating view comes under Remove View update Window Manager if ((x_cord >= x_bound_left && x_cord <= x_bound_right) && y_cord >= y_bound_top) { inBounded = true; int x_cord_remove = (int) ((szWindow.x - (remove_img_height * 1.5)) / 2); int y_cord_remove = (int) (szWindow.y - ((remove_img_width * 1.5) + getStatusBarHeight())); if (remove_image_view.getLayoutParams().height == remove_img_height) { remove_image_view.getLayoutParams().height = (int) (remove_img_height * 1.5); remove_image_view.getLayoutParams().width = (int) (remove_img_width * 1.5); WindowManager.LayoutParams param_remove = (WindowManager.LayoutParams) removeFloatingWidgetView.getLayoutParams(); param_remove.x = x_cord_remove; param_remove.y = y_cord_remove; mWindowManager.updateViewLayout(removeFloatingWidgetView, param_remove); } layoutParams.x = x_cord_remove + (Math.abs(removeFloatingWidgetView.getWidth() - mFloatingWidgetView.getWidth())) / 2; layoutParams.y = y_cord_remove + (Math.abs(removeFloatingWidgetView.getHeight() - mFloatingWidgetView.getHeight())) / 2; //Update the layout with new X & Y coordinate mWindowManager.updateViewLayout(mFloatingWidgetView, layoutParams); break; } else { //If Floating window gets out of the Remove view update Remove view again inBounded = false; remove_image_view.getLayoutParams().height = remove_img_height; remove_image_view.getLayoutParams().width = remove_img_width; onFloatingWidgetClick(); } } layoutParams.x = x_cord_Destination; layoutParams.y = y_cord_Destination; //Update the layout with new X & Y coordinate mWindowManager.updateViewLayout(mFloatingWidgetView, layoutParams); return true; } return false; } }); } private void implementClickListeners() { mFloatingWidgetView.findViewById(R.id.close_floating_view).setOnClickListener(this); mFloatingWidgetView.findViewById(R.id.close_expanded_view).setOnClickListener(this); mFloatingWidgetView.findViewById(R.id.open_activity_button).setOnClickListener(this); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.close_floating_view: //close the service and remove the from from the window stopSelf(); break; case R.id.close_expanded_view: collapsedView.setVisibility(View.VISIBLE); expandedView.setVisibility(View.GONE); break; case R.id.open_activity_button: //open the activity and stop service Intent intent = new Intent(FloatingWidgetService.this, MainActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); //close the service and remove view from the view hierarchy stopSelf(); break; } } /* on Floating Widget Long Click, increase the size of remove view as it look like taking focus */ private void onFloatingWidgetLongClick() { //Get remove Floating view params WindowManager.LayoutParams removeParams = (WindowManager.LayoutParams) removeFloatingWidgetView.getLayoutParams(); //get x and y coordinates of remove view int x_cord = (szWindow.x - removeFloatingWidgetView.getWidth()) / 2; int y_cord = szWindow.y - (removeFloatingWidgetView.getHeight() + getStatusBarHeight()); removeParams.x = x_cord; removeParams.y = y_cord; //Update Remove view params mWindowManager.updateViewLayout(removeFloatingWidgetView, removeParams); } /* Reset position of Floating Widget view on dragging */ private void resetPosition(int x_cord_now) { if (x_cord_now <= szWindow.x / 2) { isLeft = true; moveToLeft(x_cord_now); } else { isLeft = false; moveToRight(x_cord_now); } } /* Method to move the Floating widget view to Left */ private void moveToLeft(final int current_x_cord) { final int x = szWindow.x - current_x_cord; new CountDownTimer(500, 5) { //get params of Floating Widget view WindowManager.LayoutParams mParams = (WindowManager.LayoutParams) mFloatingWidgetView.getLayoutParams(); public void onTick(long t) { long step = (500 - t) / 5; mParams.x = 0 - (int) (current_x_cord * current_x_cord * step); //If you want bounce effect uncomment below line and comment above line // mParams.x = 0 - (int) (double) bounceValue(step, x); //Update window manager for Floating Widget mWindowManager.updateViewLayout(mFloatingWidgetView, mParams); } public void onFinish() { mParams.x = 0; //Update window manager for Floating Widget mWindowManager.updateViewLayout(mFloatingWidgetView, mParams); } }.start(); } /* Method to move the Floating widget view to Right */ private void moveToRight(final int current_x_cord) { new CountDownTimer(500, 5) { //get params of Floating Widget view WindowManager.LayoutParams mParams = (WindowManager.LayoutParams) mFloatingWidgetView.getLayoutParams(); public void onTick(long t) { long step = (500 - t) / 5; mParams.x = (int) (szWindow.x + (current_x_cord * current_x_cord * step) - mFloatingWidgetView.getWidth()); //If you want bounce effect uncomment below line and comment above line // mParams.x = szWindow.x + (int) (double) bounceValue(step, x_cord_now) - mFloatingWidgetView.getWidth(); //Update window manager for Floating Widget mWindowManager.updateViewLayout(mFloatingWidgetView, mParams); } public void onFinish() { mParams.x = szWindow.x - mFloatingWidgetView.getWidth(); //Update window manager for Floating Widget mWindowManager.updateViewLayout(mFloatingWidgetView, mParams); } }.start(); } /* Get Bounce value if you want to make bounce effect to your Floating Widget */ private double bounceValue(long step, long scale) { double value = scale * java.lang.Math.exp(-0.055 * step) * java.lang.Math.cos(0.08 * step); return value; } /* Detect if the floating view is collapsed or expanded */ private boolean isViewCollapsed() { return mFloatingWidgetView == null || mFloatingWidgetView.findViewById(R.id.collapse_view).getVisibility() == View.VISIBLE; } /* return status bar height on basis of device display metrics */ private int getStatusBarHeight() { return (int) Math.ceil(25 * getApplicationContext().getResources().getDisplayMetrics().density); } /* Update Floating Widget view coordinates on Configuration change */ @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); getWindowManagerDefaultDisplay(); WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) mFloatingWidgetView.getLayoutParams(); if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { if (layoutParams.y + (mFloatingWidgetView.getHeight() + getStatusBarHeight()) > szWindow.y) { layoutParams.y = szWindow.y - (mFloatingWidgetView.getHeight() + getStatusBarHeight()); mWindowManager.updateViewLayout(mFloatingWidgetView, layoutParams); } if (layoutParams.x != 0 && layoutParams.x < szWindow.x) { resetPosition(szWindow.x); } } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { if (layoutParams.x > szWindow.x) { resetPosition(szWindow.x); } } } /* on Floating widget click show expanded view */ private void onFloatingWidgetClick() { 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); } } @Override public void onDestroy() { super.onDestroy(); /* on destroy remove both view from window manager */ if (mFloatingWidgetView != null) mWindowManager.removeView(mFloatingWidgetView); if (removeFloatingWidgetView != null) mWindowManager.removeView(removeFloatingWidgetView); } } |
10. Now to Handle Overdraw permission for displaying Floating View over other apps, we need to start the FloatingWidgetService.java.
Before that, we need to check if the application has android.permission.SYSTEM_ALERT_WINDOW permission or not? For android version <= API22, this permission is granted by default. But for the android versions running API>22 ,we need to check for the permission run-time. If the permission is not available, we will open permission management screen to allow the user to grant permission using Settings.ACTION_MANAGE_OVERLAY_PERMISSION intent action. This will open below screen facilitate user to grant android.permission.SYSTEM_ALERT_WINDOW permission.
Below is code for the MainActivity.java that will display the Floating Widget when button is clicked by checking the SYSTEM_ALERT_WINDOW permission.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
package com.floatingwidgetchathead_demo; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.Toast; public class MainActivity extends AppCompatActivity { /* Permission request code to draw over other apps */ private static final int DRAW_OVER_OTHER_APP_PERMISSION_REQUEST_CODE = 1222; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } /* start floating widget service */ public void createFloatingWidget(View view) { //Check if the application has draw over other apps permission or not? //This permission is by default available for API<23. But for API > 23 //you have to ask for the permission in runtime. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) { //If the draw over permission is not available open the settings screen //to grant the permission. Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())); startActivityForResult(intent, DRAW_OVER_OTHER_APP_PERMISSION_REQUEST_CODE); } else //If permission is granted start floating widget service startFloatingWidgetService(); } /* Start Floating widget service and finish current activity */ private void startFloatingWidgetService() { startService(new Intent(MainActivity.this, FloatingWidgetService.class)); finish(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == DRAW_OVER_OTHER_APP_PERMISSION_REQUEST_CODE) { //Check if the permission is granted or not. if (resultCode == RESULT_OK) //If permission granted start floating widget service startFloatingWidgetService(); else //Permission is not available then display toast Toast.makeText(this, getResources().getString(R.string.draw_other_app_permission_denied), Toast.LENGTH_SHORT).show(); } else { super.onActivityResult(requestCode, resultCode, data); } } } |
11. Finally add the FloatingWidgetService.java in your AndroidManifest.xml.
1 2 3 4 5 |
<!-- Declare FloatingWidget Service over here and set enabled true --> <service android:name=".FloatingWidgetService" android:enabled="true" android:exported="false" /> |
12. Woohooo we are all ready. Now you can also create any kind of Floating Widget for your applications.
Thanks.
Subscribe to us and get the latest news.
27 Comments
Neeraja
Wednesday, August 30th, 2017How can we make the widget to appear even after removing the app from ‘recent apps’? I want the service to be running until even after closing the app. And, the widget should be active until user manually removes it. How can we achieve this?
Dr. Droid
Wednesday, August 30th, 2017Hi Neeraja,
Put the below code into your service class, this code will your widget run even after you close your app or clear app from recent:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
Thanks
Pushpendra Kumar
Thursday, July 16th, 2020I have tried, But it is not working for me. If application killed from recent application. Then widget also getting close. Please let me know the solution.
Yusuf
Thursday, September 7th, 2017Thanks for this awesome tutorial. Sir can you answer me please. I have EditText in my expanded view, is it possible to open soft keyboard when edittext focused? I can’t open the soft keyboard in OS home screen
Raghav Satyadev
Thursday, April 12th, 2018Thank you sir, for your great tutorial, however I found a bug in it which has been solved in this demo : https://github.com/raghavsatyadev/ChatBubbleDemo
Bug:
1. Apps not working on API 27
also I tried to improve the design. There also an issue in which chat bubble dragging is not smooth, and after 500 milliseconds expanded view gets visible even when we have removed our touch from the bubble.
Raghav Satyadev
Thursday, April 12th, 2018I solved that dragging issue (had to remove coding for that removeView)
Ashim Shrestha
Wednesday, May 2nd, 2018Hello, I wanna add some Button after chat head is clicked. can you help me with that?
Dr. Droid
Wednesday, May 2nd, 2018Hi Ashim,
Can you exactly tell me what you want and where you are facing issue?
Thanks
Ashim Shrestha
Monday, May 7th, 2018Hello Dr.Droid
I m having a problem with the intent function from open_activity_button. I m trying to intent the open_activity_button to the expand container layout.
and I could not find the code for the expand container layout to show only chat head layout
Dr. Droid
Monday, May 7th, 2018Hi Ashim,
Can you show me your code that you had done. So that i can look into it and fix it.
Thanks
Mohit Singla
Tuesday, July 24th, 2018Hi,
How does the X, and Y works,
I want to set the default launch of the window at bottom right, and also i need some margin around the window. When i am trying to set X and Y cordinates its not working. Only the gravity works, but then X and Y is not working. This breaks the floating code as well. Can you please check this on priority.
//FLAG helps to show floating screen on different OS versions.
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 view to the window
layoutParams = new WindowManager.LayoutParams(
pipWidth,
pipHeight,
LAYOUT_FLAG,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
);
//layoutParams.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
//layoutParams.gravity = Gravity.BOTTOM | Gravity.RIGHT;
//layoutParams.x = 40;
//layoutParams.y = 40;
//add floating view to the window UI
//mWindowManager.updateViewLayout(floatingWidgetView, layoutParams);
mWindowManager.addView(floatingWidgetView, layoutParams);
Mohit Singla
Wednesday, July 25th, 2018Hi did u got a chance to check this please on priority.
Yuri
Friday, September 7th, 2018Great tutorial. Thank you so much!
John
Saturday, February 16th, 2019Is it possible to make this as cordova plugin and call from webview with passing data to create floating widget?
Dr. Droid
Saturday, February 16th, 2019Hi John,
Actually i don’t have any knowledge about Cordova Plugins. So you can try but i don’t think so it will work as it is Native Code.
Thanks
Mostafijur
Saturday, March 23rd, 2019When I stay locked, when it comes to chat head, but when the phone is unlocked, the chat head will be hidden, how to do it?
Dr. Droid
Saturday, March 23rd, 2019Hi Mostafijur,
Actually I didn’t tried this scenario let me try this and figure out the solution.
Thanks
Amanpreet
Friday, May 31st, 2019Floating widget closes after some time how to keep it alive.
Dr. Droid
Friday, May 31st, 2019Hi Amanpreet,
It may be due to service is getting killed when app is in background from Oreo.
Check this link: https://developer.android.com/about/versions/oreo/background .
Thanks
moatasem
Tuesday, October 8th, 2019when i try to put EditeText the keyboard dose not appear what can i do please ….!!
Dr. Droid
Wednesday, October 9th, 2019Hi Moatasem,
You can check the same kind of issue here: https://stackoverflow.com/questions/27488569/unable-to-getfocus-and-start-editing-an-edittext-in-type-system-alert-window.
Google has introduced Bubbles recently which you can use instead of this in a much more easy way.
Here is the link : https://developer.android.com/guide/topics/ui/bubbles
Thanks
paul keum
Sunday, January 12th, 2020I want to put screenshot button inside of floating widget, but i’m having trouble using getwindow(), cause we extends service in this example. Is there any solution that we can use getwindow() or similiar one in service? I have looked every stackoverflow, but there is no clear solution. Please help
Dr. Droid
Monday, January 13th, 2020Hi Paul,
While taking a screenshot are you getting any error? If yes, then please share that error with me. I ll look into it.
Thanks
Yasiru
Thursday, February 6th, 2020Great tutorial , Thanks For that , I have question i need to create floating widget like this to show sticky notes on android screen but i need to generate multiple widgets when new note is create single note in the app taking as single floating widget if you can explain me to how to that it is great help for me thank you
Dr. Droid
Thursday, February 6th, 2020Hi Yasiru,
You cannot make Floating widget in Round shape like this.
Please check this link to create floating widget : https://www.androhub.com/android-home-screen-widgets/
Thanks
Jonathan Silva
Sunday, May 31st, 2020Good night, I’m not able to prevent the icon from rising with the content
https://stackoverflow.com/questions/62095947/how-to-prevent-relativelayout-from-rising-with-linearlayout
Help me, please
Dr. Droid
Wednesday, July 29th, 2020Hi Jonathan,
I have already added my solution to your StackOverflow question. Did you check?
Thanks