├── .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 | 
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 |
19 |
20 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Android Bubbles Example
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/test/java/com/rodrigopontes/androidbubblesexample/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.rodrigopontes.androidbubblesexample;
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 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:2.1.0'
9 |
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | jcenter()
18 | }
19 | }
20 |
21 | task clean(type: Delete) {
22 | delete rootProject.buildDir
23 | }
24 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ziacto/AndroidBubbles/1b03f2b04dda29a973b3cb68ba41ef208c5280e7/demo.gif
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':androidbubbles'
2 |
--------------------------------------------------------------------------------