├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── io │ │ └── github │ │ └── vickychijwani │ │ └── bubblenote │ │ ├── BubbleNoteActivity.java │ │ ├── BubbleNoteService.java │ │ └── Utils.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── layout │ ├── activity_bubble_note.xml │ └── bubble.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/* 4 | .DS_Store 5 | /build 6 | **/*.iml 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Vicky Chijwani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vickychijwani/BubbleNote/7a6723f9b30cd77815728c3618e74193317a7c13/README.md -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 19 5 | buildToolsVersion "20.0.0" 6 | 7 | defaultConfig { 8 | applicationId "io.github.vickychijwani.bubblenote" 9 | minSdkVersion 14 10 | targetSdkVersion 19 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | 15 | signingConfigs { 16 | release { 17 | storeFile file(System.getenv("KEYSTORE")) 18 | storePassword System.getenv("KEYSTORE_PASSWORD") 19 | keyAlias System.getenv("KEY_ALIAS") 20 | keyPassword System.getenv("KEY_PASSWORD") 21 | } 22 | } 23 | 24 | buildTypes { 25 | release { 26 | signingConfig signingConfigs.release 27 | runProguard true 28 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 29 | } 30 | } 31 | } 32 | 33 | dependencies { 34 | compile 'com.facebook.rebound:rebound:0.3.4' 35 | compile fileTree(dir: 'libs', include: ['*.jar']) 36 | } 37 | -------------------------------------------------------------------------------- /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 /opt/android-studio/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/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vickychijwani/bubblenote/BubbleNoteActivity.java: -------------------------------------------------------------------------------- 1 | package io.github.vickychijwani.bubblenote; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | import android.widget.Button; 8 | import android.widget.SeekBar; 9 | 10 | public class BubbleNoteActivity extends Activity implements SeekBar.OnSeekBarChangeListener { 11 | 12 | public static final String TAG = "BubbleNoteActivity"; 13 | 14 | private Button mShowChatHead; 15 | private SeekBar mSpringTensionSlider; 16 | private SeekBar mSpringFrictionSlider; 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.activity_bubble_note); 22 | 23 | mShowChatHead = (Button) findViewById(R.id.show_bubble); 24 | mSpringTensionSlider = (SeekBar) findViewById(R.id.spring_tension); 25 | mSpringFrictionSlider = (SeekBar) findViewById(R.id.spring_friction); 26 | 27 | mShowChatHead.setOnClickListener(new View.OnClickListener() { 28 | @Override 29 | public void onClick(View v) { 30 | Intent i = new Intent(getApplicationContext(), BubbleNoteService.class); 31 | startService(i); 32 | } 33 | }); 34 | 35 | mSpringTensionSlider.setProgress(BubbleNoteService.sSpringTension); 36 | mSpringFrictionSlider.setProgress(BubbleNoteService.sSpringFriction); 37 | 38 | mSpringTensionSlider.setOnSeekBarChangeListener(this); 39 | mSpringFrictionSlider.setOnSeekBarChangeListener(this); 40 | } 41 | 42 | @Override 43 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 44 | if (seekBar == mSpringTensionSlider) { 45 | BubbleNoteService.sSpringTension = progress; 46 | } else if (seekBar == mSpringFrictionSlider) { 47 | BubbleNoteService.sSpringFriction = progress; 48 | } 49 | BubbleNoteService.setSpringConfig(); 50 | } 51 | 52 | @Override 53 | public void onStartTrackingTouch(SeekBar seekBar) { 54 | 55 | } 56 | 57 | @Override 58 | public void onStopTrackingTouch(SeekBar seekBar) { 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vickychijwani/bubblenote/BubbleNoteService.java: -------------------------------------------------------------------------------- 1 | package io.github.vickychijwani.bubblenote; 2 | 3 | import android.app.Service; 4 | import android.content.Intent; 5 | import android.graphics.PixelFormat; 6 | import android.os.Build; 7 | import android.os.IBinder; 8 | import android.view.Gravity; 9 | import android.view.LayoutInflater; 10 | import android.view.MotionEvent; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.view.ViewTreeObserver; 14 | import android.view.WindowManager; 15 | 16 | import com.facebook.rebound.Spring; 17 | import com.facebook.rebound.SpringConfig; 18 | import com.facebook.rebound.SpringListener; 19 | import com.facebook.rebound.SpringSystem; 20 | import com.facebook.rebound.SpringUtil; 21 | 22 | public class BubbleNoteService extends Service { 23 | 24 | public static final String TAG = "BubbleNoteService"; 25 | private static final int MOVE_THRESHOLD = 100; // square of the threshold distance in pixels 26 | 27 | private WindowManager mWindowManager; 28 | private ViewGroup mBubble; 29 | private View mContent; 30 | 31 | private boolean mbExpanded = false; 32 | private boolean mbMoved = false; 33 | private int[] mPos = {0, -20}; 34 | 35 | private static Spring sBubbleSpring; 36 | private static Spring sContentSpring; 37 | public static int sSpringTension = 200; 38 | public static int sSpringFriction = 20; 39 | 40 | public static void setSpringConfig() { 41 | SpringConfig config = sBubbleSpring.getSpringConfig(); 42 | config.tension = sSpringTension; 43 | config.friction = sSpringFriction; 44 | } 45 | 46 | @Override 47 | public IBinder onBind(Intent intent) { 48 | // Not used 49 | return null; 50 | } 51 | 52 | @Override public void onCreate() { 53 | super.onCreate(); 54 | 55 | mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE); 56 | 57 | LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); 58 | mBubble = (ViewGroup) inflater.inflate(R.layout.bubble, null, false); 59 | 60 | mContent = mBubble.findViewById(R.id.content); 61 | mContent.setScaleX(0.0f); 62 | mContent.setScaleY(0.0f); 63 | ViewGroup.LayoutParams contentParams = mContent.getLayoutParams(); 64 | contentParams.width = Utils.getScreenWidth(this); 65 | contentParams.height = Utils.getScreenHeight(this) - getResources().getDimensionPixelOffset(R.dimen.bubble_height); 66 | mContent.setLayoutParams(contentParams); 67 | 68 | mBubble.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 69 | @Override 70 | public void onGlobalLayout() { 71 | mContent.setPivotX(mBubble.findViewById(R.id.bubble).getWidth() / 2); 72 | if (Build.VERSION.SDK_INT >= 16) { 73 | mBubble.getViewTreeObserver().removeOnGlobalLayoutListener(this); 74 | } else { 75 | mBubble.getViewTreeObserver().removeGlobalOnLayoutListener(this); 76 | } 77 | } 78 | }); 79 | 80 | final WindowManager.LayoutParams params = new WindowManager.LayoutParams( 81 | WindowManager.LayoutParams.WRAP_CONTENT, 82 | WindowManager.LayoutParams.WRAP_CONTENT, 83 | WindowManager.LayoutParams.TYPE_PHONE, 84 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS 85 | | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 86 | | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, 87 | PixelFormat.TRANSLUCENT); 88 | params.gravity = Gravity.TOP | Gravity.LEFT; 89 | params.x = mPos[0]; 90 | params.y = mPos[1]; 91 | params.dimAmount = 0.6f; 92 | 93 | 94 | SpringSystem system = SpringSystem.create(); 95 | SpringConfig springConfig = new SpringConfig(sSpringTension, sSpringFriction); 96 | 97 | sContentSpring = system.createSpring(); 98 | sContentSpring.setSpringConfig(springConfig); 99 | sContentSpring.setCurrentValue(0.0); 100 | sContentSpring.addListener(new SpringListener() { 101 | @Override 102 | public void onSpringUpdate(Spring spring) { 103 | float value = (float) spring.getCurrentValue(); 104 | float clampedValue = (float) SpringUtil.clamp(value, 0.0, 1.0); 105 | mContent.setScaleX(value); 106 | mContent.setScaleY(value); 107 | mContent.setAlpha(clampedValue); 108 | } 109 | 110 | @Override 111 | public void onSpringAtRest(Spring spring) { 112 | mContent.setLayerType(View.LAYER_TYPE_NONE, null); 113 | if (spring.currentValueIsApproximately(0.0)) { 114 | hideContent(); 115 | } 116 | } 117 | 118 | @Override 119 | public void onSpringActivate(Spring spring) { 120 | mContent.setLayerType(View.LAYER_TYPE_HARDWARE, null); 121 | } 122 | 123 | @Override 124 | public void onSpringEndStateChange(Spring spring) { 125 | 126 | } 127 | }); 128 | 129 | sBubbleSpring = system.createSpring(); 130 | sBubbleSpring.setSpringConfig(springConfig); 131 | sBubbleSpring.setCurrentValue(1.0); 132 | sBubbleSpring.addListener(new SpringListener() { 133 | @Override 134 | public void onSpringUpdate(Spring spring) { 135 | double value = spring.getCurrentValue(); 136 | params.x = (int) (SpringUtil.mapValueFromRangeToRange(value, 0.0, 1.0, 0.0, mPos[0])); 137 | params.y = (int) (SpringUtil.mapValueFromRangeToRange(value, 0.0, 1.0, 0.0, mPos[1])); 138 | mWindowManager.updateViewLayout(mBubble, params); 139 | if (spring.isOvershooting() && sContentSpring.isAtRest()) { 140 | sContentSpring.setEndValue(1.0); 141 | } 142 | } 143 | 144 | @Override 145 | public void onSpringAtRest(Spring spring) { 146 | 147 | } 148 | 149 | @Override 150 | public void onSpringActivate(Spring spring) { 151 | 152 | } 153 | 154 | @Override 155 | public void onSpringEndStateChange(Spring spring) { 156 | 157 | } 158 | }); 159 | 160 | 161 | mBubble.setOnTouchListener(new View.OnTouchListener() { 162 | private int initialX; 163 | private int initialY; 164 | private float initialTouchX; 165 | private float initialTouchY; 166 | 167 | @Override 168 | public boolean onTouch(View v, MotionEvent event) { 169 | switch (event.getAction()) { 170 | case MotionEvent.ACTION_DOWN: 171 | mbMoved = false; 172 | initialX = params.x; 173 | initialY = params.y; 174 | initialTouchX = event.getRawX(); 175 | initialTouchY = event.getRawY(); 176 | showContent(); 177 | return true; 178 | case MotionEvent.ACTION_UP: 179 | if (mbMoved) return true; 180 | if (! mbExpanded) { 181 | mBubble.getLocationOnScreen(mPos); 182 | mPos[1] -= Utils.getStatusBarHeight(BubbleNoteService.this); 183 | params.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 184 | params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND; 185 | sBubbleSpring.setEndValue(0.0); 186 | } else { 187 | params.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 188 | params.flags &= ~WindowManager.LayoutParams.FLAG_DIM_BEHIND; 189 | sBubbleSpring.setEndValue(1.0); 190 | sContentSpring.setEndValue(0.0); 191 | } 192 | mbExpanded = ! mbExpanded; 193 | mWindowManager.updateViewLayout(mBubble, params); 194 | return true; 195 | case MotionEvent.ACTION_MOVE: 196 | int deltaX = (int) (event.getRawX() - initialTouchX); 197 | int deltaY = (int) (event.getRawY() - initialTouchY); 198 | params.x = initialX + deltaX; 199 | params.y = initialY + deltaY; 200 | if (deltaX * deltaX + deltaY * deltaY >= MOVE_THRESHOLD) { 201 | mbMoved = true; 202 | hideContent(); 203 | mWindowManager.updateViewLayout(mBubble, params); 204 | } 205 | return true; 206 | } 207 | return false; 208 | } 209 | }); 210 | 211 | mWindowManager.addView(mBubble, params); 212 | } 213 | 214 | @Override 215 | public void onDestroy() { 216 | super.onDestroy(); 217 | if (mBubble != null) { 218 | mWindowManager.removeView(mBubble); 219 | } 220 | } 221 | 222 | private void showContent() { 223 | mContent.setVisibility(View.VISIBLE); 224 | } 225 | 226 | private void hideContent() { 227 | mContent.setVisibility(View.GONE); 228 | } 229 | 230 | } 231 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/vickychijwani/bubblenote/Utils.java: -------------------------------------------------------------------------------- 1 | package io.github.vickychijwani.bubblenote; 2 | 3 | import android.content.Context; 4 | import android.graphics.Point; 5 | import android.view.Display; 6 | import android.view.WindowManager; 7 | 8 | public class Utils { 9 | 10 | private static Point sScreenSize = null; 11 | 12 | private Utils() {} 13 | 14 | public static int getStatusBarHeight(Context context) { 15 | int result = 0; 16 | int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); 17 | if (resourceId > 0) { 18 | result = context.getResources().getDimensionPixelSize(resourceId); 19 | } 20 | return result; 21 | } 22 | 23 | public static int getScreenWidth(Context context) { 24 | fetchScreenSize(context); 25 | return sScreenSize.x; 26 | } 27 | 28 | public static int getScreenHeight(Context context) { 29 | fetchScreenSize(context); 30 | return sScreenSize.y; 31 | } 32 | 33 | private static void fetchScreenSize(Context context) { 34 | if (sScreenSize != null) return; 35 | WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 36 | Display display = wm.getDefaultDisplay(); 37 | sScreenSize = new Point(); 38 | display.getSize(sScreenSize); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vickychijwani/BubbleNote/7a6723f9b30cd77815728c3618e74193317a7c13/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vickychijwani/BubbleNote/7a6723f9b30cd77815728c3618e74193317a7c13/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vickychijwani/BubbleNote/7a6723f9b30cd77815728c3618e74193317a7c13/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vickychijwani/BubbleNote/7a6723f9b30cd77815728c3618e74193317a7c13/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_bubble_note.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 |