├── .gitignore ├── README.md ├── androidbubbles ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── rodrigopontes │ │ └── androidbubbles │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── rodrigopontes │ │ │ └── androidbubbles │ │ │ ├── Bubble.java │ │ │ ├── BubbleBase.java │ │ │ ├── BubbleOnTapListener.java │ │ │ ├── BubbleTrash.java │ │ │ ├── BubblesManager.java │ │ │ └── BubblesProperties.java │ └── res │ │ ├── drawable-hdpi │ │ ├── bubble_trash_background.png │ │ └── bubble_trash_foreground.png │ │ ├── drawable-mdpi │ │ ├── bubble_trash_background.png │ │ └── bubble_trash_foreground.png │ │ ├── drawable-xhdpi │ │ ├── bubble_trash_background.png │ │ └── bubble_trash_foreground.png │ │ ├── drawable-xxhdpi │ │ ├── bubble_trash_background.png │ │ └── bubble_trash_foreground.png │ │ ├── drawable-xxxhdpi │ │ ├── bubble_trash_background.png │ │ └── bubble_trash_foreground.png │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── rodrigopontes │ └── androidbubbles │ └── ExampleUnitTest.java ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── rodrigopontes │ │ └── androidbubblesexample │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── rodrigopontes │ │ │ └── androidbubblesexample │ │ │ ├── MainActivity.java │ │ │ └── ScreenOrientationService.java │ └── res │ │ ├── drawable-hdpi │ │ └── example_bubble.png │ │ ├── drawable-mdpi │ │ └── example_bubble.png │ │ ├── drawable-xhdpi │ │ └── example_bubble.png │ │ ├── drawable-xxhdpi │ │ └── example_bubble.png │ │ ├── drawable-xxxhdpi │ │ └── example_bubble.png │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── rodrigopontes │ └── androidbubblesexample │ └── ExampleUnitTest.java ├── build.gradle ├── demo.gif ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .idea 10 | gradle 11 | /androidbubbles/build 12 | /androidbubbles/libs 13 | /app/build 14 | /app/libs -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidBubbles 2 | Android bubbles recreates the chat bubbles as implemented by Facebook. It focuses on smooth animations for a smiliar user experience, and extremely easy implementation. 3 | 4 | ![](https://github.com/RodrigoDLPontes/AndroidBubbles/blob/master/demo.gif?raw=true) 5 | 6 | (Higher quality version can be found [here](https://www.youtube.com/watch?v=E2966AjH6ew)) 7 | 8 | ## Usage 9 | 10 | You can start using Android Bubbles in 3 simple steps. 11 | 12 | ### 1. Add project dependency 13 | 14 | First, add Android Bubbles to your project dependencies. 15 | 16 | Make sure you have jcenter as one of your repositories... 17 | ```groovy 18 | repositories { 19 | ... 20 | jcenter() 21 | } 22 | ``` 23 | ...and add Android Bubbles to your dependencies. 24 | ```groovy 25 | dependencies { 26 | ... 27 | compile 'com.rodrigopontes:android-bubbles:0.1.0' 28 | } 29 | ``` 30 | 31 | ### 2. Create a BubblesManager 32 | 33 | Create a BubblesManager using a Context. 34 | ```java 35 | public class MainActivity extends Activity { 36 | 37 | BubblesManager bubblesManager; 38 | 39 | @Override 40 | public void onCreate(Bundle savedInstanceState) { 41 | super.onCreate(savedInstanceState); 42 | setContentView(R.layout.main_activity); 43 | 44 | bubblesManager = BubblesManager.create(this); 45 | } 46 | } 47 | ``` 48 | After you create it, you can also retrieve it with 49 | ```java 50 | bubblesManager = BubblesManager.getManager(); 51 | ``` 52 | 53 | ### 3. Add Bubble 54 | 55 | Add a bubble to BubblesManager 56 | ```java 57 | ImageView imageView = new ImageView(this); 58 | imageView.setImageResource(R.drawable.my_image); 59 | Bubble bubble = new Bubble(imageView); 60 | bubblesManager.addBubble(bubble); 61 | ``` 62 | You now have your first Android Bubble! 63 | 64 | ## Other methods 65 | 66 | ### Handle Bubble taps 67 | 68 | You can handle Bubble taps just as you would with a Button. 69 | 70 | ```java 71 | bubble.setBubbleOnTapListener(new BubbleOnTapListener { 72 | 73 | @Override 74 | public void onTap(Bubble.BubblePosition bubblePosition) { 75 | Log.d("Bubbles", "Hello World!"); 76 | } 77 | } 78 | ``` 79 | 80 | The Bubble's position can be retrieved from 81 | 82 | ```java 83 | bubblePosition.x 84 | bubblePosition.y 85 | ``` 86 | 87 | You can also implement 88 | ```java 89 | public void onTapConfirmed(Bubble.BubblePosition) 90 | public void onDoubleTap(Bubble.BubblePosition) 91 | ``` 92 | 93 | ### Remove Bubble 94 | 95 | You can remove Bubbles programatically with 96 | 97 | ```java 98 | bubblesManager.removeBubble(bubble); 99 | ``` 100 | ### Create Bubbles easily 101 | 102 | You can save a few lines of code by using Bubble's convenience contructors. 103 | ```java 104 | new Bubble(imageBitmap); 105 | new Bubble(drawable); 106 | new Bubble(resourceId); 107 | new Bubble(imageUri); 108 | ``` 109 | 110 | ### Set Bubble's image size 111 | 112 | You can customize the size of the Bubble by using 113 | ```java 114 | bubble.setImageViewSize(width, height); 115 | ``` 116 | 117 | ### Check if BubblesManager exists 118 | 119 | To avoid problems with activity recreations, you can wrap your BubblesManager creation with 120 | 121 | ```java 122 | if(BubblesManager.exists()) { 123 | bubblesManager = BubblesManager.getManager(); 124 | } else { 125 | bubblesManager = BubblesManeger.create(this); 126 | } 127 | ``` 128 | 129 | ### Handle screen rotations 130 | 131 | Screen rotations must be handled by the Activity that implements BubblesManager. That is made simple with 132 | 133 | ```java 134 | bubblesManager.updateConfiguration(); 135 | ``` 136 | 137 | ## Highly customizable 138 | 139 | You can easily customize Android Bubble's feel by tweaking the fields in BubblesProperties. 140 | 141 | ## Share your project! 142 | 143 | Implemented Android Bubbles for your project? Send it to rodrigo.dl.pontes@gmail.com and I'll share it here! 144 | 145 | ## License 146 | 147 | Copyright 2016 Rodrigo Pontes 148 | 149 | Licensed under the Apache License, Version 2.0 (the "License"); 150 | you may not use this file except in compliance with the License. 151 | You may obtain a copy of the License at 152 | 153 | http://www.apache.org/licenses/LICENSE-2.0 154 | 155 | Unless required by applicable law or agreed to in writing, software 156 | distributed under the License is distributed on an "AS IS" BASIS, 157 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 158 | See the License for the specific language governing permissions and 159 | limitations under the License. 160 | -------------------------------------------------------------------------------- /androidbubbles/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /androidbubbles/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | ext { 4 | PUBLISH_GROUP_ID = 'com.rodrigopontes' 5 | PUBLISH_ARTIFACT_ID = 'android-bubbles' 6 | PUBLISH_VERSION = '0.1.1' 7 | } 8 | 9 | android { 10 | compileSdkVersion 23 11 | buildToolsVersion "23.0.3" 12 | 13 | defaultConfig { 14 | minSdkVersion 15 15 | targetSdkVersion 23 16 | versionCode 2 17 | versionName "0.1.1" 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | compile fileTree(dir: 'libs', include: ['*.jar']) 29 | testCompile 'junit:junit:4.12' 30 | compile 'com.android.support:appcompat-v7:23.4.0' 31 | } 32 | 33 | apply from: 'https://raw.githubusercontent.com/blundell/release-android-library/master/android-release-aar.gradle' -------------------------------------------------------------------------------- /androidbubbles/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/rodrigopontes/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /androidbubbles/src/androidTest/java/com/rodrigopontes/androidbubbles/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.rodrigopontes.androidbubbles; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /androidbubbles/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /androidbubbles/src/main/java/com/rodrigopontes/androidbubbles/Bubble.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Rodrigo Deleu Lopes Pontes 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.rodrigopontes.androidbubbles; 18 | 19 | import android.graphics.Bitmap; 20 | import android.graphics.drawable.Drawable; 21 | import android.net.Uri; 22 | import android.widget.ImageView; 23 | 24 | public class Bubble extends BubbleBase { 25 | 26 | private BubbleOnTapListener bubbleOnClickListener; 27 | 28 | /** 29 | * Create Bubble with given ImageView 30 | */ 31 | public Bubble(ImageView imageView) { 32 | super(); 33 | super.setImageView(imageView); 34 | bubbleOnClickListener = null; 35 | } 36 | 37 | /** 38 | * Convenience constructor to create Bubble with Bitmap 39 | */ 40 | public Bubble(Bitmap imageBitmap) { 41 | super(); 42 | ImageView imageView = new ImageView(BubblesManager.getManager().getContext()); 43 | imageView.setImageBitmap(imageBitmap); 44 | super.setImageView(imageView); 45 | bubbleOnClickListener = null; 46 | } 47 | 48 | /** 49 | * Convenience constructor to create Bubble with Drawable 50 | */ 51 | public Bubble(Drawable drawable) { 52 | super(); 53 | ImageView imageView = new ImageView(BubblesManager.getManager().getContext()); 54 | imageView.setImageDrawable(drawable); 55 | super.setImageView(imageView); 56 | bubbleOnClickListener = null; 57 | } 58 | 59 | /** 60 | * Convenience constructor to create Bubble with Resource ID 61 | */ 62 | public Bubble(int resId) { 63 | super(); 64 | ImageView imageView = new ImageView(BubblesManager.getManager().getContext()); 65 | imageView.setImageResource(resId); 66 | super.setImageView(imageView); 67 | bubbleOnClickListener = null; 68 | } 69 | 70 | /** 71 | * Convenience constructor to create Bubble with URI 72 | */ 73 | public Bubble(Uri uri) { 74 | super(); 75 | ImageView imageView = new ImageView(BubblesManager.getManager().getContext()); 76 | imageView.setImageURI(uri); 77 | super.setImageView(imageView); 78 | bubbleOnClickListener = null; 79 | } 80 | 81 | /** 82 | * Sets Bubble's ImageView size 83 | */ 84 | public void setImageViewSize(int width, int height) { 85 | super.getImageView().getLayoutParams().width = width; 86 | super.getImageView().getLayoutParams().height = height; 87 | } 88 | 89 | /** 90 | * Sets Bubble's OnTapListener 91 | */ 92 | public void setBubbleOnTapListener(BubbleOnTapListener bubbleOnClickListener) { 93 | this.bubbleOnClickListener = bubbleOnClickListener; 94 | } 95 | 96 | protected BubbleOnTapListener getBubbleOnClickListener() { 97 | return bubbleOnClickListener; 98 | } 99 | 100 | public static class BubblePosition { 101 | public int x; 102 | public int y; 103 | 104 | public BubblePosition(int x, int y) { 105 | this.x = x; 106 | this.y = y; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /androidbubbles/src/main/java/com/rodrigopontes/androidbubbles/BubbleBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Rodrigo Deleu Lopes Pontes 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.rodrigopontes.androidbubbles; 18 | 19 | import android.view.ViewGroup; 20 | import android.widget.FrameLayout; 21 | import android.widget.ImageView; 22 | 23 | public class BubbleBase { 24 | 25 | private FrameLayout frameLayout; 26 | private ImageView imageView; 27 | 28 | protected BubbleBase() { 29 | frameLayout = new FrameLayout(BubblesManager.getManager().getContext()); 30 | } 31 | 32 | protected void setImageView(ImageView imageView) { 33 | this.imageView = imageView; 34 | this.imageView.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 35 | frameLayout.addView(this.imageView); 36 | } 37 | 38 | public FrameLayout getFrameLayout() { 39 | return frameLayout; 40 | } 41 | 42 | public ImageView getImageView() { 43 | return imageView; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /androidbubbles/src/main/java/com/rodrigopontes/androidbubbles/BubbleOnTapListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Rodrigo Deleu Lopes Pontes 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.rodrigopontes.androidbubbles; 18 | 19 | public abstract class BubbleOnTapListener { 20 | 21 | public abstract void onTap(Bubble.BubblePosition bubblePosition); 22 | 23 | public void onTapConfirmed(Bubble.BubblePosition bubblePosition) {} 24 | 25 | public void onDoubleTap(Bubble.BubblePosition bubblePosition) {} 26 | } 27 | -------------------------------------------------------------------------------- /androidbubbles/src/main/java/com/rodrigopontes/androidbubbles/BubbleTrash.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Rodrigo Deleu Lopes Pontes 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.rodrigopontes.androidbubbles; 18 | 19 | import android.widget.ImageView; 20 | 21 | public class BubbleTrash extends BubbleBase { 22 | 23 | private ImageView imageViewForeground; 24 | 25 | protected BubbleTrash() { 26 | super(); 27 | ImageView imageViewBackground = new ImageView(BubblesManager.getManager().getContext()); 28 | imageViewBackground.setImageResource(R.drawable.bubble_trash_background); 29 | super.setImageView(imageViewBackground); 30 | imageViewForeground = new ImageView(BubblesManager.getManager().getContext()); 31 | imageViewForeground.setImageResource(R.drawable.bubble_trash_foreground); 32 | super.getFrameLayout().addView(imageViewForeground); 33 | } 34 | 35 | protected ImageView getImageViewForeground() { 36 | return imageViewForeground; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /androidbubbles/src/main/java/com/rodrigopontes/androidbubbles/BubblesManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Rodrigo Deleu Lopes Pontes 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.rodrigopontes.androidbubbles; 18 | 19 | import android.content.Context; 20 | import android.graphics.PixelFormat; 21 | import android.graphics.Point; 22 | import android.os.AsyncTask; 23 | import android.os.Vibrator; 24 | import android.support.v4.view.GestureDetectorCompat; 25 | import android.view.GestureDetector; 26 | import android.view.Gravity; 27 | import android.view.MotionEvent; 28 | import android.view.View; 29 | import android.view.ViewGroup; 30 | import android.view.WindowManager; 31 | import android.view.animation.ScaleAnimation; 32 | 33 | import java.util.ArrayList; 34 | 35 | public class BubblesManager { 36 | 37 | private static BubblesManager instance; 38 | private ArrayList bubblesList; 39 | private Context context; 40 | private WindowManager windowManager; 41 | private static short screenWidth; 42 | private static short screenHeight; 43 | private static short statusBarHeight; 44 | private BubbleTrash bubbleTrash; 45 | private WindowManager.LayoutParams bubbleTrashParams; 46 | private static short bubbleTrashWidth; 47 | private static short bubbleTrashHeight; 48 | private AsyncTask trashEnterInXAnimation; 49 | private AsyncTask trashEnterInYAnimation; 50 | private AsyncTask trashExitAnimation; 51 | private AsyncTask trashFollowXMovement; 52 | private AsyncTask trashFollowYMovement; 53 | private boolean trashOnScreen; 54 | 55 | /** 56 | * Creates BubblesManager with given Context 57 | */ 58 | public static BubblesManager create(Context context) { 59 | if(instance == null) { 60 | instance = new BubblesManager(context); 61 | instance.initialize(); 62 | instance.bubblesList = new ArrayList<>(); 63 | return instance; 64 | } else { 65 | throw new BubblesManagerNotNullException(); 66 | } 67 | } 68 | 69 | /** 70 | * Returns created BubblesManager 71 | */ 72 | public static BubblesManager getManager() { 73 | if(instance != null) { 74 | return instance; 75 | } else { 76 | throw new NullPointerException("BubblesManager has not yet been created! Use .create() first."); 77 | } 78 | } 79 | 80 | private BubblesManager(Context context) { 81 | this.context = context; 82 | } 83 | 84 | // Initializes BubbleManager 85 | 86 | private void initialize() { 87 | windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); 88 | Point point = new Point(); 89 | windowManager.getDefaultDisplay().getSize(point); 90 | screenWidth = (short)point.x; 91 | screenHeight = (short)point.y; 92 | int statusBarResId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); 93 | statusBarHeight = (short)context.getResources().getDimensionPixelSize(statusBarResId); 94 | 95 | bubbleTrash = new BubbleTrash(); 96 | bubbleTrashParams = new WindowManager.LayoutParams( 97 | ViewGroup.LayoutParams.WRAP_CONTENT, 98 | ViewGroup.LayoutParams.WRAP_CONTENT, 99 | WindowManager.LayoutParams.TYPE_PHONE, 100 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | 101 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | 102 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, 103 | PixelFormat.TRANSLUCENT); 104 | bubbleTrashParams.gravity = Gravity.TOP | Gravity.START; 105 | bubbleTrash.getFrameLayout().measure(screenWidth, screenHeight); 106 | bubbleTrashWidth = (short)(bubbleTrash.getFrameLayout().getMeasuredWidth() * BubblesProperties.TRASH_INCREASE_SIZE); 107 | bubbleTrashHeight = (short)(bubbleTrash.getFrameLayout().getMeasuredHeight() * BubblesProperties.TRASH_INCREASE_SIZE); 108 | bubbleTrash.getFrameLayout().setMinimumWidth(bubbleTrashWidth); 109 | bubbleTrash.getFrameLayout().setMinimumHeight(bubbleTrashHeight); 110 | bubbleTrash.getImageView().measure(screenWidth, screenHeight); 111 | bubbleTrash.getImageView().setX(bubbleTrashWidth / 2 - bubbleTrash.getImageView().getMeasuredWidth() / 2); 112 | bubbleTrash.getImageView().setY(bubbleTrashHeight / 2 - bubbleTrash.getImageView().getMeasuredHeight() / 2); 113 | bubbleTrash.getImageViewForeground().measure(screenWidth, screenHeight); 114 | bubbleTrash.getImageViewForeground().setX(bubbleTrashWidth / 2 - bubbleTrash.getImageViewForeground().getMeasuredWidth() / 2); 115 | bubbleTrash.getImageViewForeground().setY(bubbleTrashHeight / 2 - bubbleTrash.getImageViewForeground().getMeasuredHeight() / 2); 116 | bubbleTrashParams.x = (screenWidth - bubbleTrashWidth) / 2; 117 | bubbleTrashParams.y = screenHeight + BubblesProperties.TRASH_ENTER_SPEED; 118 | windowManager.addView(bubbleTrash.getFrameLayout(), bubbleTrashParams); 119 | } 120 | 121 | /** 122 | * Checks whether BubblesManager has been created 123 | */ 124 | public static boolean exists() { 125 | return instance != null; 126 | } 127 | 128 | /** 129 | * Adds new Bubble to BubblesManager 130 | */ 131 | public void addBubble(Bubble bubble) { 132 | if(windowManager != null) { 133 | WindowManager.LayoutParams params = new WindowManager.LayoutParams( 134 | WindowManager.LayoutParams.WRAP_CONTENT, 135 | WindowManager.LayoutParams.WRAP_CONTENT, 136 | WindowManager.LayoutParams.TYPE_PHONE, 137 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, 138 | PixelFormat.TRANSLUCENT); 139 | params.gravity = Gravity.TOP | Gravity.START; 140 | params.y = screenHeight / BubblesProperties.BUBBLE_ENTER_Y_POSITION; 141 | BubbleController bubbleController = new BubbleController(bubble, params); 142 | bubble.getFrameLayout().setOnTouchListener(bubbleController.new BubbleTouchListener()); 143 | bubblesList.add(bubbleController); 144 | windowManager.addView(bubble.getFrameLayout(), params); 145 | bubbleController.createdAnimation = new SpringAnimation( 146 | new AnimationPosition(BubblesProperties.MODE_STATIC_POSITION, screenWidth + BubblesProperties.BUBBLE_ENTER_SPEED), 147 | new AnimationPosition(BubblesProperties.MODE_STATIC_POSITION, screenWidth - (int)(bubbleController.bubbleWidth * BubblesProperties.BUBBLE_EDGE_OFFSET_RIGHT)), 148 | params, 149 | BubblesProperties.AXIS_X, 150 | BubblesProperties.SPRING_ANIMATION_LONG_DURATION, 151 | bubble).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 152 | } else { 153 | throw new NullPointerException("BubblesManager has not yet been created! Use .create() first."); 154 | } 155 | } 156 | 157 | /** 158 | * Removes Bubble from BubblesManager 159 | */ 160 | public void removeBubble(BubbleController bubbleController) { 161 | if(windowManager != null) { 162 | bubblesList.remove(bubbleController); 163 | windowManager.removeViewImmediate(bubbleController.bubble.getFrameLayout()); 164 | } else { 165 | throw new NullPointerException("BubblesManager has not yet been created! Use .create() first."); 166 | } 167 | } 168 | 169 | /** 170 | * Updates BubblesManager for orientation change 171 | */ 172 | public void updateConfiguration() { 173 | int oldScreenWidth = screenWidth; 174 | int oldScreenHeight = screenHeight; 175 | cancelAsyncTasks(trashEnterInXAnimation, trashEnterInYAnimation, 176 | trashExitAnimation, trashFollowXMovement, trashFollowYMovement); 177 | windowManager.removeViewImmediate(bubbleTrash.getFrameLayout()); 178 | for(BubbleController bubbleController : bubblesList) { 179 | cancelAsyncTasks(bubbleController.createdAnimation, 180 | bubbleController.edgeAnimation, 181 | bubbleController.flingAnimation, 182 | bubbleController.bubbleEnteringTrashInXAnimation, 183 | bubbleController.bubbleEnteringTrashInYAnimation, 184 | bubbleController.bubbleExitingTrashInXAnimation, 185 | bubbleController.bubbleExitingTrashInYAnimation); 186 | windowManager.removeViewImmediate(bubbleController.bubble.getFrameLayout()); 187 | } 188 | initialize(); 189 | for(BubbleController bubbleController : bubblesList) { 190 | BubbleController newBubbleController = new BubbleController(bubbleController.bubble, bubbleController.params); 191 | bubbleController.params.y = (int)(((float)bubbleController.params.y / (float)oldScreenHeight) * (float)screenHeight); 192 | if(bubbleController.params.x < oldScreenWidth / 2) { 193 | bubbleController.params.x = (int)(-newBubbleController.bubbleWidth * BubblesProperties.BUBBLE_EDGE_OFFSET_LEFT); 194 | } else { 195 | bubbleController.params.x = (int)(screenWidth - newBubbleController.bubbleWidth * BubblesProperties.BUBBLE_EDGE_OFFSET_RIGHT); 196 | } 197 | newBubbleController.bubble.getFrameLayout().setOnTouchListener(newBubbleController.new BubbleTouchListener()); 198 | bubblesList.set(bubblesList.indexOf(bubbleController), newBubbleController); 199 | windowManager.addView(newBubbleController.bubble.getFrameLayout(), bubbleController.params); 200 | } 201 | } 202 | 203 | // Helper methods 204 | 205 | protected Context getContext() { 206 | return context; 207 | } 208 | 209 | private void playScaleAnimation(ScaleAnimation scaleAnimation, int duration, BubbleBase bubble) { 210 | scaleAnimation.setDuration(duration); 211 | scaleAnimation.setFillAfter(true); 212 | bubble.getImageView().startAnimation(scaleAnimation); 213 | } 214 | 215 | private short getCenter(int paramValue, int size) { 216 | return (short)(paramValue + size / 2); 217 | } 218 | 219 | private void cancelAsyncTasks(AsyncTask... animations) { 220 | for(AsyncTask animation : animations) { 221 | if(animation != null && !animation.getStatus().name().equals("FINISHED")) { 222 | animation.cancel(true); 223 | } 224 | } 225 | } 226 | 227 | private boolean haveAsyncTasksFinished(AsyncTask... animations) { 228 | for(AsyncTask animation : animations) { 229 | if(!(animation == null || !animation.getStatus().name().equals("RUNNING"))) { 230 | return false; 231 | } 232 | } 233 | return true; 234 | } 235 | 236 | // Controls all bubble behaviours 237 | 238 | private class BubbleController { 239 | 240 | private Bubble bubble; 241 | private short bubbleWidth; 242 | private short bubbleHeight; 243 | private short leftEdgeLimit; 244 | private short rightEdgeLimit; 245 | private short bottomEdgeLimit; 246 | private WindowManager.LayoutParams params; 247 | private GestureDetectorCompat bubbleGestureDetector; 248 | private AsyncTask createdAnimation; 249 | private AsyncTask edgeAnimation; 250 | private AsyncTask flingAnimation; 251 | private AsyncTask bubbleEnteringTrashInXAnimation; 252 | private AsyncTask bubbleEnteringTrashInYAnimation; 253 | private AsyncTask bubbleExitingTrashInXAnimation; 254 | private AsyncTask bubbleExitingTrashInYAnimation; 255 | private boolean inTrash; 256 | 257 | private BubbleController(Bubble bubble, WindowManager.LayoutParams params) { 258 | this.bubble = bubble; 259 | bubble.getFrameLayout().measure(screenWidth, screenHeight); 260 | bubbleWidth = (short)bubble.getFrameLayout().getMeasuredWidth(); 261 | bubbleHeight = (short)bubble.getFrameLayout().getMeasuredHeight(); 262 | leftEdgeLimit = (short)(-bubbleWidth * BubblesProperties.BUBBLE_EDGE_OFFSET_LEFT); 263 | rightEdgeLimit = (short)(screenWidth - bubbleWidth * BubblesProperties.BUBBLE_EDGE_OFFSET_RIGHT); 264 | bottomEdgeLimit = (short)(screenHeight - statusBarHeight - bubbleHeight); 265 | this.params = params; 266 | bubbleGestureDetector = new GestureDetectorCompat(context, new BubbleGestureListener()); 267 | } 268 | 269 | class BubbleTouchListener implements View.OnTouchListener { 270 | 271 | private int initialX; 272 | private int initialY; 273 | private float initialTouchX; 274 | private float initialTouchY; 275 | 276 | @Override 277 | public boolean onTouch(View v, final MotionEvent event) { 278 | bubbleGestureDetector.onTouchEvent(event); 279 | switch(event.getAction()) { 280 | 281 | /* 282 | ACTION DOWN 283 | */ 284 | 285 | case MotionEvent.ACTION_DOWN: 286 | // Initializes bubble movement and shrinks bubble icon 287 | cancelAsyncTasks(createdAnimation, edgeAnimation, flingAnimation); 288 | initialX = params.x; 289 | initialY = params.y; 290 | initialTouchX = event.getRawX(); 291 | initialTouchY = event.getRawY(); 292 | playScaleAnimation(new ScaleAnimation(1, BubblesProperties.BUBBLE_SHRINK_SIZE, 293 | 1, BubblesProperties.BUBBLE_SHRINK_SIZE, 294 | bubbleWidth / 2, bubbleHeight / 2), 295 | BubblesProperties.SCALE_ANIMATION_DURATION, 296 | bubble); 297 | return true; 298 | 299 | /* 300 | ACTION CANCEL & UP 301 | */ 302 | 303 | case MotionEvent.ACTION_CANCEL: 304 | case MotionEvent.ACTION_UP: 305 | if(!inTrash) { 306 | // If bubble not inside trash, resize it back to normal, move it to screen edge, and hide trash 307 | playScaleAnimation(new ScaleAnimation(BubblesProperties.BUBBLE_SHRINK_SIZE, 1, 308 | BubblesProperties.BUBBLE_SHRINK_SIZE, 1, 309 | bubbleWidth / 2, bubbleHeight / 2), 310 | BubblesProperties.SCALE_ANIMATION_DURATION, 311 | bubble); 312 | if(getCenter(params.x, bubbleWidth) < screenWidth / 2) { 313 | edgeAnimation = new SpringAnimation( 314 | new AnimationPosition(BubblesProperties.MODE_STATIC_POSITION, params.x), 315 | new AnimationPosition(BubblesProperties.MODE_STATIC_POSITION, leftEdgeLimit), 316 | params, 317 | BubblesProperties.AXIS_X, 318 | BubblesProperties.SPRING_ANIMATION_LONG_DURATION, 319 | bubble).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 320 | } else { 321 | edgeAnimation = new SpringAnimation( 322 | new AnimationPosition(BubblesProperties.MODE_STATIC_POSITION, params.x), 323 | new AnimationPosition(BubblesProperties.MODE_STATIC_POSITION, rightEdgeLimit), 324 | params, 325 | BubblesProperties.AXIS_X, 326 | BubblesProperties.SPRING_ANIMATION_LONG_DURATION, 327 | bubble).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 328 | } 329 | if(trashOnScreen) { 330 | trashOnScreen = false; 331 | cancelAsyncTasks(trashEnterInXAnimation, trashEnterInYAnimation, trashFollowXMovement, trashFollowYMovement); 332 | trashExitAnimation = new ExitAnimation().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 333 | } 334 | } else { 335 | // If bubble inside trash, hide both and delete bubble 336 | trashOnScreen = false; 337 | cancelAsyncTasks(trashEnterInXAnimation, trashEnterInYAnimation, bubbleEnteringTrashInXAnimation, 338 | bubbleEnteringTrashInYAnimation, flingAnimation, trashFollowXMovement, trashFollowYMovement); 339 | trashExitAnimation = new ExitAnimation() { 340 | @Override 341 | protected void onPostExecute(Void aVoid) { 342 | super.onPostExecute(aVoid); 343 | playScaleAnimation(new ScaleAnimation(BubblesProperties.TRASH_INCREASE_SIZE, 1, 344 | BubblesProperties.TRASH_INCREASE_SIZE, 1, 345 | bubbleTrashWidth / 2, bubbleTrashHeight / 2), 346 | BubblesProperties.SCALE_ANIMATION_DURATION, 347 | bubbleTrash); 348 | bubbleTrashParams.y = screenHeight + BubblesProperties.TRASH_ENTER_SPEED; 349 | windowManager.updateViewLayout(bubbleTrash.getFrameLayout(), bubbleTrashParams); 350 | } 351 | }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 352 | new ExitAnimation(params, bubble){ 353 | 354 | @Override 355 | protected void onPostExecute(Void aVoid) { 356 | super.onPostExecute(aVoid); 357 | removeBubble(BubbleController.this); 358 | } 359 | }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 360 | } 361 | return true; 362 | 363 | /* 364 | ACTION MOVE 365 | */ 366 | 367 | case MotionEvent.ACTION_MOVE: 368 | // Minimum threshold to consider movement 369 | if(Math.abs(event.getRawX() - initialTouchX) > 10 || Math.abs(event.getRawY() - initialTouchY) > 10) { 370 | // Display trash 371 | if(!trashOnScreen) { 372 | trashOnScreen = true; 373 | cancelAsyncTasks(trashExitAnimation); 374 | trashEnterInXAnimation = new SpringAnimation( 375 | new AnimationPosition(BubblesProperties.MODE_STATIC_POSITION, bubbleTrashParams.x), 376 | new AnimationPosition(BubblesProperties.MODE_TRASH_POSITION, params, BubblesProperties.AXIS_X, bubbleWidth), 377 | bubbleTrashParams, 378 | BubblesProperties.AXIS_X, 379 | BubblesProperties.SPRING_ANIMATION_LONG_DURATION, 380 | bubbleTrash).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 381 | trashEnterInYAnimation = new SpringAnimation( 382 | new AnimationPosition(BubblesProperties.MODE_STATIC_POSITION, bubbleTrashParams.y), 383 | new AnimationPosition(BubblesProperties.MODE_TRASH_POSITION, params, BubblesProperties.AXIS_Y), 384 | bubbleTrashParams, 385 | BubblesProperties.AXIS_Y, 386 | BubblesProperties.SPRING_ANIMATION_LONG_DURATION, 387 | bubbleTrash).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 388 | } 389 | if(!inTrash) { 390 | //Bubble is not inside trash 391 | if(haveAsyncTasksFinished(bubbleExitingTrashInYAnimation)) { 392 | // Move trash alongside the bubble 393 | if(haveAsyncTasksFinished(trashEnterInYAnimation, bubbleEnteringTrashInYAnimation, trashFollowYMovement, trashExitAnimation)) { 394 | bubbleTrashParams.x = screenWidth / 2 - bubbleTrashWidth / 2 + 395 | ((params.x + bubbleWidth / 2) - screenWidth / 2) / 10; 396 | bubbleTrashParams.y = (int)(screenHeight * 0.725f) + params.y / 10; 397 | windowManager.updateViewLayout(bubbleTrash.getFrameLayout(), bubbleTrashParams); 398 | } 399 | // Bubble movement 400 | params.x = initialX + (int)(event.getRawX() - initialTouchX); 401 | params.y = initialY + (int)(event.getRawY() - initialTouchY); 402 | // Limit movement to screen edges 403 | if(params.x < leftEdgeLimit) { 404 | params.x = leftEdgeLimit; 405 | } else if(params.x > rightEdgeLimit) { 406 | params.x = rightEdgeLimit; 407 | } 408 | if(params.y < 0) { 409 | params.y = 0; 410 | } else if(params.y > bottomEdgeLimit) { 411 | params.y = bottomEdgeLimit; 412 | } 413 | windowManager.updateViewLayout(bubble.getFrameLayout(), params); 414 | // Check if bubble has entered trash bounds 415 | if(Math.abs(getCenter(params.y, bubbleHeight) - getCenter(bubbleTrashParams.y, bubbleTrashHeight)) < bubbleHeight * 2) { 416 | if(!inTrash) { 417 | // Send bubble to trash 418 | inTrash = true; 419 | ((Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE)).vibrate(BubblesProperties.TRASH_VIBRATION_DURATION); 420 | playScaleAnimation(new ScaleAnimation(1, BubblesProperties.TRASH_INCREASE_SIZE, 421 | 1, BubblesProperties.TRASH_INCREASE_SIZE, 422 | bubbleTrashWidth / 2, bubbleTrashHeight / 2), 423 | BubblesProperties.SCALE_ANIMATION_DURATION, 424 | bubbleTrash); 425 | cancelAsyncTasks(trashEnterInXAnimation, trashEnterInYAnimation, trashFollowXMovement, trashFollowYMovement); 426 | bubbleEnteringTrashInXAnimation = new SpringAnimation( 427 | new AnimationPosition(BubblesProperties.MODE_STATIC_POSITION, params.x), 428 | new AnimationPosition(BubblesProperties.MODE_STATIC_POSITION, getCenter(bubbleTrashParams.x, bubbleTrashWidth) - bubbleWidth / 2), 429 | params, 430 | BubblesProperties.AXIS_X, 431 | BubblesProperties.SPRING_ANIMATION_LONG_DURATION, 432 | bubble).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 433 | bubbleEnteringTrashInYAnimation = new SpringAnimation( 434 | new AnimationPosition(BubblesProperties.MODE_STATIC_POSITION, params.y), 435 | new AnimationPosition(BubblesProperties.MODE_STATIC_POSITION, getCenter(bubbleTrashParams.y, bubbleHeight) - bubbleHeight / 2), 436 | params, 437 | BubblesProperties.AXIS_Y, 438 | BubblesProperties.SPRING_ANIMATION_LONG_DURATION, 439 | bubble).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 440 | } 441 | } 442 | } 443 | } else { 444 | // Bubble is inside trash, and user has moved finger away from trash 445 | if(!(Math.abs(event.getRawY() - getCenter(bubbleTrashParams.y, bubbleTrashHeight)) < bubbleHeight * 2.1f)) { 446 | // Remove bubble from trash 447 | inTrash = false; 448 | playScaleAnimation(new ScaleAnimation(BubblesProperties.TRASH_INCREASE_SIZE, 1, 449 | BubblesProperties.TRASH_INCREASE_SIZE, 1, 450 | bubbleWidth / 2, bubbleHeight / 2), 451 | BubblesProperties.SCALE_ANIMATION_DURATION, 452 | bubbleTrash); 453 | cancelAsyncTasks(bubbleEnteringTrashInXAnimation, bubbleEnteringTrashInYAnimation); 454 | bubbleExitingTrashInXAnimation = new SpringAnimation( 455 | new AnimationPosition(BubblesProperties.MODE_PARAMS_POSITION, params, BubblesProperties.AXIS_X), 456 | new AnimationPosition(BubblesProperties.MODE_TOUCH_POSITION, event, initialX, initialTouchX, BubblesProperties.AXIS_X), 457 | params, 458 | BubblesProperties.AXIS_X, 459 | BubblesProperties.SPRING_ANIMATION_SHORT_DURATION, 460 | bubble).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 461 | bubbleExitingTrashInYAnimation = new SpringAnimation( 462 | new AnimationPosition(BubblesProperties.MODE_PARAMS_POSITION, params, BubblesProperties.AXIS_Y), 463 | new AnimationPosition(BubblesProperties.MODE_TOUCH_POSITION, event, initialY, initialTouchY, BubblesProperties.AXIS_Y), 464 | params, 465 | BubblesProperties.AXIS_Y, 466 | BubblesProperties.SPRING_ANIMATION_SHORT_DURATION, 467 | bubble) { 468 | 469 | @Override 470 | protected void onPostExecute(Void aVoid) { 471 | trashFollowXMovement = new SpringAnimation( 472 | new AnimationPosition(BubblesProperties.MODE_STATIC_POSITION, bubbleTrashParams.x), 473 | new AnimationPosition(BubblesProperties.MODE_TRASH_POSITION, params, BubblesProperties.AXIS_X, bubbleWidth), 474 | bubbleTrashParams, 475 | BubblesProperties.AXIS_X, 476 | BubblesProperties.SPRING_ANIMATION_LONG_DURATION, 477 | bubbleTrash).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 478 | trashFollowYMovement = new SpringAnimation( 479 | new AnimationPosition(BubblesProperties.MODE_STATIC_POSITION, bubbleTrashParams.y), 480 | new AnimationPosition(BubblesProperties.MODE_TRASH_POSITION, params, BubblesProperties.AXIS_Y), 481 | bubbleTrashParams, 482 | BubblesProperties.AXIS_Y, 483 | BubblesProperties.SPRING_ANIMATION_LONG_DURATION, 484 | bubbleTrash).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 485 | } 486 | }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 487 | } 488 | } 489 | } 490 | return true; 491 | } 492 | return false; 493 | } 494 | } 495 | 496 | class BubbleGestureListener extends GestureDetector.SimpleOnGestureListener { 497 | 498 | // Fling inertia 499 | @Override 500 | public boolean onFling(final MotionEvent e1, final MotionEvent e2, float velocityX, float velocityY) { 501 | flingAnimation = new AsyncTask() { 502 | 503 | @Override 504 | protected Void doInBackground(Void... params) { 505 | float yAcceleration = e2.getY() - e1.getY(); 506 | while(Math.abs(yAcceleration) > 1) { 507 | publishProgress((int)yAcceleration); 508 | yAcceleration *= BubblesProperties.FLING_DECELERATION_FACTOR; 509 | try { 510 | Thread.sleep(10); 511 | } catch(InterruptedException e) { 512 | e.printStackTrace(); 513 | } 514 | } 515 | return null; 516 | } 517 | 518 | @Override 519 | protected void onProgressUpdate(Integer... values) { 520 | params.y += values[0]; 521 | if(params.y < 0) { 522 | params.y = 0; 523 | } else if(params.y > bottomEdgeLimit) { 524 | params.y = bottomEdgeLimit; 525 | } 526 | windowManager.updateViewLayout(bubble.getFrameLayout(), params); 527 | } 528 | }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 529 | return super.onFling(e1, e2, velocityX, velocityY); 530 | } 531 | 532 | // Bubble has been tapped 533 | @Override 534 | public boolean onSingleTapUp(MotionEvent e) { 535 | if(bubble.getBubbleOnClickListener() != null) { 536 | bubble.getBubbleOnClickListener().onTap(new Bubble.BubblePosition(params.x, params.y)); 537 | } 538 | return super.onSingleTapUp(e); 539 | } 540 | 541 | @Override 542 | public boolean onSingleTapConfirmed(MotionEvent e) { 543 | if(bubble.getBubbleOnClickListener() != null) { 544 | bubble.getBubbleOnClickListener().onTapConfirmed(new Bubble.BubblePosition(params.x, params.y)); 545 | } 546 | return super.onSingleTapConfirmed(e); 547 | } 548 | 549 | @Override 550 | public boolean onDoubleTap(MotionEvent e) { 551 | if(bubble.getBubbleOnClickListener() != null) { 552 | bubble.getBubbleOnClickListener().onDoubleTap(new Bubble.BubblePosition(params.x, params.y)); 553 | } 554 | return super.onDoubleTap(e); 555 | } 556 | } 557 | } 558 | 559 | // Wrapper for positions to use in animations 560 | private class AnimationPosition { 561 | 562 | private byte mode; 563 | private int value; 564 | private WindowManager.LayoutParams params; 565 | private byte axis; 566 | private int bubbleWidth; 567 | private MotionEvent event; 568 | private int initialPos; 569 | private float initialTouchPos; 570 | 571 | // Regular static position (use mode MODE_STATIC_POSITION) 572 | protected AnimationPosition(byte mode, int position) { 573 | this.mode = mode; 574 | value = position; 575 | } 576 | 577 | // Dynamic position based on params (use mode MODE_PARAMS_POSITION) 578 | protected AnimationPosition(byte mode, WindowManager.LayoutParams params, byte axis) { 579 | this.mode = mode; 580 | this.params = params; 581 | this.axis = axis; 582 | } 583 | 584 | // Dynamic position based on params with offset for use with trash (use mode MODE_TRASH_POSITION) 585 | protected AnimationPosition(byte mode, WindowManager.LayoutParams params, byte axis, int bubbleWidth) { 586 | this.mode = mode; 587 | this.params = params; 588 | this.bubbleWidth = bubbleWidth; 589 | this.axis = axis; 590 | } 591 | 592 | // Dynamic position based on touch (use mode MODE_TOUCH_POSITION) 593 | protected AnimationPosition(byte mode, MotionEvent event, int initialPos, float initialTouchPos, byte axis) { 594 | this.mode = mode; 595 | this.event = event; 596 | this.initialPos = initialPos; 597 | this.initialTouchPos = initialTouchPos; 598 | this.axis = axis; 599 | } 600 | 601 | protected int get() { 602 | switch(mode) { 603 | case 0: // Regular static position 604 | return value; 605 | case 1: // Dynamic position based on params 606 | if(axis == 0) { 607 | return params.x; 608 | } else { 609 | return params.y; 610 | } 611 | case 2: // Dynamic position based on params with offset for use with trash 612 | if(axis == 0) { 613 | return screenWidth / 2 - bubbleTrashWidth / 2 + ((params.x + bubbleWidth / 2) - screenWidth / 2) / 10; 614 | } else { 615 | return (int)(screenHeight * 0.725f) + params.y / 10; 616 | } 617 | case 3: // Dynamic position based on touch 618 | if(axis == 0) { 619 | return initialPos + (int)(event.getRawX() - initialTouchPos); 620 | } else { 621 | return initialPos + (int)(event.getRawY() - initialTouchPos); 622 | } 623 | } 624 | // Should never get here 625 | return -1; 626 | } 627 | } 628 | 629 | //Defines spring animation 630 | private class SpringAnimation extends AsyncTask { 631 | 632 | private AnimationPosition initialPosition; 633 | private AnimationPosition finalPosition; 634 | private WindowManager.LayoutParams animParams; 635 | private byte axis; 636 | private float duration; 637 | private BubbleBase bubble; 638 | 639 | protected SpringAnimation(AnimationPosition initialPosition, 640 | AnimationPosition finalPosition, 641 | WindowManager.LayoutParams animParams, 642 | byte axis, float duration, BubbleBase bubble) { 643 | this.initialPosition = initialPosition; 644 | this.finalPosition = finalPosition; 645 | this.animParams = animParams; 646 | this.axis = axis; 647 | this.duration = duration; 648 | this.bubble = bubble; 649 | } 650 | 651 | @Override 652 | protected Void doInBackground(Void... params) { 653 | for(float i = 1 ; i < duration ; i += 0.1f) { 654 | /* 655 | Equation that defines spring animation 656 | 657 | cos(x - 1) 658 | b + (------------ * (a - b)) 659 | x^2 660 | 661 | Where x is time, a is initial position and b is final position 662 | */ 663 | publishProgress((int)(finalPosition.get() + 664 | ((Math.cos(i - 1) / Math.pow(i, BubblesProperties.SPRING_ANIMATION_RESISTANCE)) * 665 | (initialPosition.get() - finalPosition.get())))); 666 | try { 667 | Thread.sleep(10); 668 | } catch(InterruptedException e) { 669 | return null; 670 | } 671 | } 672 | return null; 673 | } 674 | 675 | @Override 676 | protected void onProgressUpdate(Integer... values) { 677 | if(axis == 0) { 678 | animParams.x = values[0]; 679 | } else { 680 | animParams.y = values[0]; 681 | } 682 | windowManager.updateViewLayout(bubble.getFrameLayout(), animParams); 683 | } 684 | } 685 | 686 | // Simple animation for hiding bubbles 687 | private class ExitAnimation extends AsyncTask { 688 | 689 | Bubble bubble; 690 | WindowManager.LayoutParams params; 691 | 692 | protected ExitAnimation() { 693 | params = bubbleTrashParams; 694 | } 695 | 696 | protected ExitAnimation(WindowManager.LayoutParams params, Bubble bubble) { 697 | this.params = params; 698 | this.bubble = bubble; 699 | } 700 | 701 | @Override 702 | protected Void doInBackground(Void... args) { 703 | int initialY; 704 | if(bubble != null) { 705 | initialY = params.y; 706 | } else { 707 | initialY = bubbleTrashParams.y; 708 | } 709 | for(int i = 1 ; params.y < screenHeight ; i += BubblesProperties.EXIT_ANIMATION_SPEED) { 710 | if(isCancelled()) return null; 711 | publishProgress((int)(initialY + i * i)); 712 | try { 713 | Thread.sleep(10); 714 | } catch(InterruptedException e) { 715 | return null; 716 | } 717 | } 718 | return null; 719 | } 720 | 721 | @Override 722 | protected void onProgressUpdate(Integer... values) { 723 | if(bubble != null) { 724 | params.y = values[0]; 725 | windowManager.updateViewLayout(bubble.getFrameLayout(), params); 726 | } else { 727 | bubbleTrashParams.y = values[0]; 728 | windowManager.updateViewLayout(bubbleTrash.getFrameLayout(), bubbleTrashParams); 729 | } 730 | } 731 | } 732 | 733 | private static class BubblesManagerNotNullException extends RuntimeException { 734 | 735 | public BubblesManagerNotNullException() { 736 | super("BubblesManager already exists! Use .getManager() instead."); 737 | } 738 | } 739 | } -------------------------------------------------------------------------------- /androidbubbles/src/main/java/com/rodrigopontes/androidbubbles/BubblesProperties.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Rodrigo Deleu Lopes Pontes 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.rodrigopontes.androidbubbles; 18 | 19 | public class BubblesProperties { 20 | 21 | // Internal parameters. Do not change! 22 | final static byte MODE_STATIC_POSITION = 0; 23 | final static byte MODE_PARAMS_POSITION = 1; 24 | final static byte MODE_TRASH_POSITION = 2; 25 | final static byte MODE_TOUCH_POSITION = 3; 26 | final static byte AXIS_X = 0; 27 | final static byte AXIS_Y = 1; 28 | 29 | // Customizable fields 30 | final static byte BUBBLE_ENTER_Y_POSITION = 8; // Fraction of screen height bubble will enter. 31 | final static short BUBBLE_ENTER_SPEED = 200; 32 | final static float BUBBLE_EDGE_OFFSET_LEFT = 0.2f; // Percentage of bubble that will remain off-screen 33 | final static float BUBBLE_EDGE_OFFSET_RIGHT = 0.8f; // Percentage of bubble that will remain on-screen (should be 1 minus above value) 34 | final static float BUBBLE_SHRINK_SIZE = 0.8f; // Percentage of size after shrink 35 | 36 | final static short TRASH_ENTER_SPEED = 200; 37 | final static float TRASH_INCREASE_SIZE = 1.2f; // Percentage of size after increase 38 | final static short TRASH_VIBRATION_DURATION = 50; 39 | 40 | final static float SPRING_ANIMATION_RESISTANCE = 2f; 41 | final static byte SPRING_ANIMATION_LONG_DURATION = 12; // Use roots of spring function in BubblesManager 42 | final static float SPRING_ANIMATION_SHORT_DURATION = 2.5f; // Use roots of spring function in BubblesManager 43 | 44 | final static short SCALE_ANIMATION_DURATION = 100; 45 | 46 | final static float FLING_DECELERATION_FACTOR = 0.9f; 47 | 48 | final static byte EXIT_ANIMATION_SPEED = 1; 49 | } 50 | -------------------------------------------------------------------------------- /androidbubbles/src/main/res/drawable-hdpi/bubble_trash_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/androidbubbles/src/main/res/drawable-hdpi/bubble_trash_background.png -------------------------------------------------------------------------------- /androidbubbles/src/main/res/drawable-hdpi/bubble_trash_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/androidbubbles/src/main/res/drawable-hdpi/bubble_trash_foreground.png -------------------------------------------------------------------------------- /androidbubbles/src/main/res/drawable-mdpi/bubble_trash_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/androidbubbles/src/main/res/drawable-mdpi/bubble_trash_background.png -------------------------------------------------------------------------------- /androidbubbles/src/main/res/drawable-mdpi/bubble_trash_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/androidbubbles/src/main/res/drawable-mdpi/bubble_trash_foreground.png -------------------------------------------------------------------------------- /androidbubbles/src/main/res/drawable-xhdpi/bubble_trash_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/androidbubbles/src/main/res/drawable-xhdpi/bubble_trash_background.png -------------------------------------------------------------------------------- /androidbubbles/src/main/res/drawable-xhdpi/bubble_trash_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/androidbubbles/src/main/res/drawable-xhdpi/bubble_trash_foreground.png -------------------------------------------------------------------------------- /androidbubbles/src/main/res/drawable-xxhdpi/bubble_trash_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/androidbubbles/src/main/res/drawable-xxhdpi/bubble_trash_background.png -------------------------------------------------------------------------------- /androidbubbles/src/main/res/drawable-xxhdpi/bubble_trash_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/androidbubbles/src/main/res/drawable-xxhdpi/bubble_trash_foreground.png -------------------------------------------------------------------------------- /androidbubbles/src/main/res/drawable-xxxhdpi/bubble_trash_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/androidbubbles/src/main/res/drawable-xxxhdpi/bubble_trash_background.png -------------------------------------------------------------------------------- /androidbubbles/src/main/res/drawable-xxxhdpi/bubble_trash_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/androidbubbles/src/main/res/drawable-xxxhdpi/bubble_trash_foreground.png -------------------------------------------------------------------------------- /androidbubbles/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Android Bubbles 3 | 4 | -------------------------------------------------------------------------------- /androidbubbles/src/test/java/com/rodrigopontes/androidbubbles/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.rodrigopontes.androidbubbles; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * To work on unit tests, switch the Test Artifact in the Build Variants view. 9 | */ 10 | public class ExampleUnitTest { 11 | @Test 12 | public void addition_isCorrect() throws Exception { 13 | assertEquals(4, 2 + 2); 14 | } 15 | } -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.3" 6 | 7 | defaultConfig { 8 | applicationId "com.rodrigopontes.androidbubblesexample" 9 | minSdkVersion 15 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | testCompile 'junit:junit:4.12' 25 | compile 'com.android.support:appcompat-v7:23.4.0' 26 | compile project(':androidbubbles') 27 | } 28 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/rodrigopontes/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/rodrigopontes/androidbubblesexample/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.rodrigopontes.androidbubblesexample; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/rodrigopontes/androidbubblesexample/MainActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Rodrigo Deleu Lopes Pontes 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.rodrigopontes.androidbubblesexample; 18 | 19 | import android.content.Intent; 20 | import android.support.v7.app.AppCompatActivity; 21 | import android.os.Bundle; 22 | import android.util.Log; 23 | import android.view.View; 24 | 25 | import com.rodrigopontes.androidbubbles.Bubble; 26 | import com.rodrigopontes.androidbubbles.BubbleOnTapListener; 27 | import com.rodrigopontes.androidbubbles.BubblesManager; 28 | 29 | public class MainActivity extends AppCompatActivity { 30 | 31 | @Override 32 | protected void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | setContentView(R.layout.activity_main); 35 | } 36 | 37 | // Action for "Create Bubbles Manager" button 38 | public void onCreateBubblesManager(View view) { 39 | // Creates a service that wraps the creation of Bubbles Manager. 40 | // This is a good approach if you want to keep track of screen orientation changes 41 | // while your app is in the background. 42 | startService(new Intent(this, ScreenOrientationService.class)); 43 | } 44 | 45 | // Action for "Add Bubble" button 46 | public void onAddBubble(View view) { 47 | Bubble bubble = new Bubble(R.drawable.example_bubble); 48 | // Bubble Listener example. 49 | bubble.setBubbleOnTapListener(new BubbleOnTapListener() { 50 | @Override 51 | public void onTap(Bubble.BubblePosition bubblePosition) { 52 | Log.d("Debug", "Bubble tapped at: x - " + bubblePosition.x + " | y - " + bubblePosition.y); 53 | } 54 | 55 | @Override 56 | public void onTapConfirmed(Bubble.BubblePosition bubblePosition) { 57 | Log.d("Debug", "Bubble tapped confirmed at: x - " + bubblePosition.x + " | y - " + bubblePosition.y); 58 | } 59 | 60 | @Override 61 | public void onDoubleTap(Bubble.BubblePosition bubblePosition) { 62 | Log.d("Debug", "Bubble double tapped at: x - " + bubblePosition.x + " | y - " + bubblePosition.y); 63 | } 64 | }); 65 | BubblesManager.getManager().addBubble(bubble); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/rodrigopontes/androidbubblesexample/ScreenOrientationService.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Rodrigo Deleu Lopes Pontes 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.rodrigopontes.androidbubblesexample; 18 | 19 | import android.app.Service; 20 | import android.content.BroadcastReceiver; 21 | import android.content.Context; 22 | import android.content.Intent; 23 | import android.content.IntentFilter; 24 | import android.os.Build; 25 | import android.os.IBinder; 26 | import android.provider.Settings; 27 | 28 | import com.rodrigopontes.androidbubbles.BubblesManager; 29 | 30 | public class ScreenOrientationService extends Service { 31 | 32 | BubblesManager bubblesManager; 33 | 34 | public ScreenOrientationService() {} 35 | 36 | @Override 37 | public IBinder onBind(Intent intent) { 38 | return null; 39 | } 40 | 41 | @Override 42 | public void onCreate() { 43 | super.onCreate(); 44 | // If running Android M, ask for drawing permission if necessary 45 | if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 46 | if(Settings.canDrawOverlays(getApplicationContext())) { 47 | if(BubblesManager.exists()) { 48 | bubblesManager = BubblesManager.getManager(); 49 | } else { 50 | bubblesManager = BubblesManager.create(getApplicationContext()); 51 | } 52 | IntentFilter intentFilter = new IntentFilter(); 53 | intentFilter.addAction("android.intent.action.CONFIGURATION_CHANGED"); 54 | registerReceiver(new BroadcastReceiver() { 55 | @Override 56 | public void onReceive(Context context, Intent intent) { 57 | if(intent.getAction().equals("android.intent.action.CONFIGURATION_CHANGED")) { 58 | bubblesManager.updateConfiguration(); 59 | } 60 | } 61 | }, intentFilter); 62 | } else { 63 | Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); 64 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 65 | startActivity(intent); 66 | stopSelf(); 67 | } 68 | } else { 69 | if(BubblesManager.exists()) { 70 | bubblesManager = BubblesManager.getManager(); 71 | } else { 72 | bubblesManager = BubblesManager.create(getApplicationContext()); 73 | } 74 | IntentFilter intentFilter = new IntentFilter(); 75 | intentFilter.addAction("android.intent.action.CONFIGURATION_CHANGED"); 76 | registerReceiver(new BroadcastReceiver() { 77 | @Override 78 | public void onReceive(Context context, Intent intent) { 79 | if(intent.getAction().equals("android.intent.action.CONFIGURATION_CHANGED")) { 80 | bubblesManager.updateConfiguration(); 81 | } 82 | } 83 | }, intentFilter); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/example_bubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/app/src/main/res/drawable-hdpi/example_bubble.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/example_bubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/app/src/main/res/drawable-mdpi/example_bubble.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/example_bubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/app/src/main/res/drawable-xhdpi/example_bubble.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/example_bubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/app/src/main/res/drawable-xxhdpi/example_bubble.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/example_bubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/app/src/main/res/drawable-xxxhdpi/example_bubble.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 |