├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── libraries │ ├── appcompat_v7_22_2_1.xml │ ├── circularimageview_1_1.xml │ ├── support_annotations_22_2_1.xml │ └── support_v4_22_2_1.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml ├── vcs.xml └── workspace.xml ├── BubbleCloudExample.iml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── dodola │ │ └── bubblecloudexample │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── dodola │ │ └── bubblecloudexample │ │ ├── MyListView.java │ │ └── TestActivity.java │ └── res │ ├── drawable │ ├── background.9.png │ ├── contact_image.png │ ├── icon.png │ └── item_circle.xml │ ├── layout │ ├── activity_main.xml │ ├── list_item.xml │ └── main.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── bubblecloud ├── .gitignore ├── bubblecloud.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── dodola │ │ └── bubblecloud │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── dodola │ │ └── bubblecloud │ │ ├── BubbleCloudView.java │ │ ├── bitmapfun │ │ ├── AsyncTask.java │ │ ├── DiskLruCache.java │ │ ├── ImageCache.java │ │ ├── ImageFetcher.java │ │ ├── ImageResizer.java │ │ ├── ImageWorker.java │ │ ├── RecyclingBitmapDrawable.java │ │ └── Utils.java │ │ └── utils │ │ ├── FileManagerImageLoader.java │ │ └── Utils.java │ └── res │ └── values │ ├── attrs.xml │ ├── dimens.xml │ └── strings.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshot └── demo.gif └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | /*/build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | BubbleCloudExample -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/libraries/appcompat_v7_22_2_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/libraries/circularimageview_1_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/libraries/support_annotations_22_2_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/support_v4_22_2_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /BubbleCloudExample.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 dodola 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BubbleCloudView(Beta) 2 | Like apple watch launcher view(仿苹果表应用表盘界面) 3 | 4 | 5 | 6 | ## Screenshots 7 | 8 | ![GIF example](screenshot/demo.gif) 9 | 10 | # Thanks 11 | - [Apple-Watch-Spring-Board](https://github.com/Dzinlife/Apple-Watch-Spring-Board) 12 | - [CircularImageView](https://github.com/Pkmmte/CircularImageView) 13 | 14 | 15 | License 16 | -------- 17 | 18 | The MIT License (MIT) 19 | 20 | Copyright (c) 2015 dodola 21 | 22 | Permission is hereby granted, free of charge, to any person obtaining a copy 23 | of this software and associated documentation files (the "Software"), to deal 24 | in the Software without restriction, including without limitation the rights 25 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 26 | copies of the Software, and to permit persons to whom the Software is 27 | furnished to do so, subject to the following conditions: 28 | 29 | The above copyright notice and this permission notice shall be included in all 30 | copies or substantial portions of the Software. 31 | 32 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 33 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 34 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 35 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 36 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 37 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 38 | SOFTWARE. 39 | 40 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | applicationId "com.dodola.bubblecloudexample" 9 | minSdkVersion 14 10 | targetSdkVersion 22 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 | compile 'com.android.support:appcompat-v7:22.2.1' 25 | compile project(':bubblecloud') 26 | } 27 | -------------------------------------------------------------------------------- /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/dodola/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/dodola/bubblecloudexample/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.dodola.bubblecloudexample; 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 | 6 | 7 | 8 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/dodola/bubblecloudexample/MyListView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2010, Sony Ericsson Mobile Communication AB. All rights reserved. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, 5 | * are permitted provided that the following conditions are met: 6 | * 7 | * * Redistributions of source code must retain the above copyright notice, this 8 | * list of conditions and the following disclaimer. 9 | * * Redistributions in binary form must reproduce the above copyright notice, 10 | * this list of conditions and the following disclaimer in the documentation 11 | * and/or other materials provided with the distribution. 12 | * * Neither the name of the Sony Ericsson Mobile Communication AB nor the names 13 | * of its contributors may be used to endorse or promote products derived from 14 | * this software without specific prior written permission. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 19 | * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 20 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 24 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 25 | * OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | package com.dodola.bubblecloudexample; 29 | 30 | import android.content.Context; 31 | import android.graphics.Bitmap; 32 | import android.graphics.Camera; 33 | import android.graphics.Canvas; 34 | import android.graphics.Color; 35 | import android.graphics.LightingColorFilter; 36 | import android.graphics.Matrix; 37 | import android.graphics.Paint; 38 | import android.graphics.Rect; 39 | import android.util.AttributeSet; 40 | import android.view.MotionEvent; 41 | import android.view.View; 42 | import android.view.ViewConfiguration; 43 | import android.widget.Adapter; 44 | import android.widget.AdapterView; 45 | 46 | import java.util.LinkedList; 47 | 48 | /** 49 | * A simple list view that displays the items as 3D blocks 50 | */ 51 | public class MyListView extends AdapterView { 52 | 53 | /** Width of the items compared to the width of the list */ 54 | private static final float ITEM_WIDTH = 0.85f; 55 | 56 | /** Space occupied by the item relative to the height of the item */ 57 | private static final float ITEM_VERTICAL_SPACE = 1.45f; 58 | 59 | /** Ambient light intensity */ 60 | private static final int AMBIENT_LIGHT = 55; 61 | 62 | /** Diffuse light intensity */ 63 | private static final int DIFFUSE_LIGHT = 200; 64 | 65 | /** Specular light intensity */ 66 | private static final float SPECULAR_LIGHT = 70; 67 | 68 | /** Shininess constant */ 69 | private static final float SHININESS = 200; 70 | 71 | /** The max intensity of the light */ 72 | private static final int MAX_INTENSITY = 0xFF; 73 | 74 | /** Amount of down scaling */ 75 | private static final float SCALE_DOWN_FACTOR = 0.15f; 76 | 77 | /** Amount to rotate during one screen length */ 78 | private static final int DEGREES_PER_SCREEN = 270; 79 | 80 | /** Represents an invalid child index */ 81 | private static final int INVALID_INDEX = -1; 82 | 83 | /** Distance to drag before we intercept touch events */ 84 | private static final int TOUCH_SCROLL_THRESHOLD = 10; 85 | 86 | /** Children added with this layout mode will be added below the last child */ 87 | private static final int LAYOUT_MODE_BELOW = 0; 88 | 89 | /** Children added with this layout mode will be added above the first child */ 90 | private static final int LAYOUT_MODE_ABOVE = 1; 91 | 92 | /** User is not touching the list */ 93 | private static final int TOUCH_STATE_RESTING = 0; 94 | 95 | /** User is touching the list and right now it's still a "click" */ 96 | private static final int TOUCH_STATE_CLICK = 1; 97 | 98 | /** User is scrolling the list */ 99 | private static final int TOUCH_STATE_SCROLL = 2; 100 | 101 | /** The adapter with all the data */ 102 | private Adapter mAdapter; 103 | 104 | /** Current touch state */ 105 | private int mTouchState = TOUCH_STATE_RESTING; 106 | 107 | /** X-coordinate of the down event */ 108 | private int mTouchStartX; 109 | 110 | /** Y-coordinate of the down event */ 111 | private int mTouchStartY; 112 | 113 | /** 114 | * The top of the first item when the touch down event was received 115 | */ 116 | private int mListTopStart; 117 | 118 | /** The current top of the first item */ 119 | private int mListTop; 120 | 121 | /** 122 | * The offset from the top of the currently first visible item to the top of 123 | * the first item 124 | */ 125 | private int mListTopOffset; 126 | 127 | /** Current rotation */ 128 | private int mListRotation; 129 | 130 | /** The adaptor position of the first visible item */ 131 | private int mFirstItemPosition; 132 | 133 | /** The adaptor position of the last visible item */ 134 | private int mLastItemPosition; 135 | 136 | /** A list of cached (re-usable) item views */ 137 | private final LinkedList mCachedItemViews = new LinkedList(); 138 | 139 | /** Used to check for long press actions */ 140 | private Runnable mLongPressRunnable; 141 | 142 | /** Reusable rect */ 143 | private Rect mRect; 144 | 145 | /** Camera used for 3D transformations */ 146 | private Camera mCamera; 147 | 148 | /** Re-usable matrix for canvas transformations */ 149 | private Matrix mMatrix; 150 | 151 | /** Paint object to draw with */ 152 | private Paint mPaint; 153 | 154 | /** true if rotation of the items is enabled */ 155 | private boolean mRotationEnabled = true; 156 | 157 | /** true if lighting of the items is enabled */ 158 | private boolean mLightEnabled = true; 159 | 160 | /** 161 | * Constructor 162 | * 163 | * @param context The context 164 | * @param attrs Attributes 165 | */ 166 | public MyListView(final Context context, final AttributeSet attrs) { 167 | super(context, attrs); 168 | } 169 | 170 | @Override 171 | public void setAdapter(final Adapter adapter) { 172 | mAdapter = adapter; 173 | removeAllViewsInLayout(); 174 | requestLayout(); 175 | } 176 | 177 | @Override 178 | public Adapter getAdapter() { 179 | return mAdapter; 180 | } 181 | 182 | @Override 183 | public void setSelection(final int position) { 184 | throw new UnsupportedOperationException("Not supported"); 185 | } 186 | 187 | @Override 188 | public View getSelectedView() { 189 | throw new UnsupportedOperationException("Not supported"); 190 | } 191 | 192 | /** 193 | * Enables and disables individual rotation of the items. 194 | * 195 | * @param enable If rotation should be enabled or not 196 | */ 197 | public void enableRotation(final boolean enable) { 198 | mRotationEnabled = enable; 199 | if (!mRotationEnabled) { 200 | mListRotation = 0; 201 | } 202 | invalidate(); 203 | } 204 | 205 | /** 206 | * Checks whether rotation is enabled 207 | * 208 | * @return true if rotation is enabled 209 | */ 210 | public boolean isRotationEnabled() { 211 | return mRotationEnabled; 212 | } 213 | 214 | /** 215 | * Enables and disables lighting of the items. 216 | * 217 | * @param enable If lighting should be enabled or not 218 | */ 219 | public void enableLight(final boolean enable) { 220 | mLightEnabled = enable; 221 | if (!mLightEnabled) { 222 | mPaint.setColorFilter(null); 223 | } else { 224 | mPaint.setAlpha(0xFF); 225 | } 226 | invalidate(); 227 | } 228 | 229 | /** 230 | * Checks whether lighting is enabled 231 | * 232 | * @return true if rotation is enabled 233 | */ 234 | public boolean isLightEnabled() { 235 | return mLightEnabled; 236 | } 237 | 238 | @Override 239 | public boolean onInterceptTouchEvent(final MotionEvent event) { 240 | switch (event.getAction()) { 241 | case MotionEvent.ACTION_DOWN: 242 | startTouch(event); 243 | return false; 244 | 245 | case MotionEvent.ACTION_MOVE: 246 | return startScrollIfNeeded(event); 247 | 248 | default: 249 | endTouch(); 250 | return false; 251 | } 252 | } 253 | 254 | @Override 255 | public boolean onTouchEvent(final MotionEvent event) { 256 | if (getChildCount() == 0) { 257 | return false; 258 | } 259 | switch (event.getAction()) { 260 | case MotionEvent.ACTION_DOWN: 261 | startTouch(event); 262 | break; 263 | 264 | case MotionEvent.ACTION_MOVE: 265 | if (mTouchState == TOUCH_STATE_CLICK) { 266 | startScrollIfNeeded(event); 267 | } 268 | if (mTouchState == TOUCH_STATE_SCROLL) { 269 | scrollList((int)event.getY() - mTouchStartY); 270 | } 271 | break; 272 | 273 | case MotionEvent.ACTION_UP: 274 | if (mTouchState == TOUCH_STATE_CLICK) { 275 | clickChildAt((int)event.getX(), (int)event.getY()); 276 | } 277 | endTouch(); 278 | break; 279 | 280 | default: 281 | endTouch(); 282 | break; 283 | } 284 | return true; 285 | } 286 | 287 | @Override 288 | protected void onLayout(final boolean changed, final int left, final int top, final int right, 289 | final int bottom) { 290 | super.onLayout(changed, left, top, right, bottom); 291 | 292 | // if we don't have an adapter, we don't need to do anything 293 | if (mAdapter == null) { 294 | return; 295 | } 296 | 297 | if (getChildCount() == 0) { 298 | mLastItemPosition = -1; 299 | fillListDown(mListTop, 0); 300 | } else { 301 | final int offset = mListTop + mListTopOffset - getChildTop(getChildAt(0)); 302 | removeNonVisibleViews(offset); 303 | fillList(offset); 304 | } 305 | 306 | positionItems(); 307 | invalidate(); 308 | } 309 | 310 | @Override 311 | protected boolean drawChild(final Canvas canvas, final View child, final long drawingTime) { 312 | // get the bitmap 313 | final Bitmap bitmap = child.getDrawingCache(); 314 | if (bitmap == null) { 315 | // if the is null for some reason, default to the standard 316 | // drawChild implementation 317 | return super.drawChild(canvas, child, drawingTime); 318 | } 319 | 320 | // get top left coordinates 321 | final int top = child.getTop(); 322 | final int left = child.getLeft(); 323 | 324 | // get centerX and centerY 325 | final int childWidth = child.getWidth(); 326 | final int childHeight = child.getHeight(); 327 | final int centerX = childWidth / 2; 328 | final int centerY = childHeight / 2; 329 | 330 | // get scale 331 | final float halfHeight = getHeight() / 2; 332 | final float distFromCenter = (top + centerY - halfHeight) / halfHeight; 333 | final float scale = (float)(1 - SCALE_DOWN_FACTOR * (1 - Math.cos(distFromCenter))); 334 | 335 | // get rotation 336 | float childRotation = mListRotation - 20 * distFromCenter; 337 | childRotation %= 90; 338 | if (childRotation < 0) { 339 | childRotation += 90; 340 | } 341 | 342 | // draw the item 343 | if (childRotation < 45) { 344 | drawFace(canvas, bitmap, top, left, centerX, centerY, scale, childRotation - 90); 345 | drawFace(canvas, bitmap, top, left, centerX, centerY, scale, childRotation); 346 | } else { 347 | drawFace(canvas, bitmap, top, left, centerX, centerY, scale, childRotation); 348 | drawFace(canvas, bitmap, top, left, centerX, centerY, scale, childRotation - 90); 349 | } 350 | 351 | return false; 352 | } 353 | 354 | /** 355 | * Draws a face of the 3D block 356 | * 357 | * @param canvas The canvas to draw on 358 | * @param view A bitmap of the view to draw 359 | * @param top Top placement of the view 360 | * @param left Left placement of the view 361 | * @param centerX Center x-coordinate of the view 362 | * @param centerY Center y-coordinate of the view 363 | * @param scale The scale to draw the view in 364 | * @param rotation The rotation of the view 365 | */ 366 | private void drawFace(final Canvas canvas, final Bitmap view, final int top, final int left, 367 | final int centerX, final int centerY, final float scale, final float rotation) { 368 | 369 | // create the camera if we haven't before 370 | if (mCamera == null) { 371 | mCamera = new Camera(); 372 | } 373 | 374 | // save the camera state 375 | mCamera.save(); 376 | 377 | // translate and then rotate the camera 378 | mCamera.translate(0, 0, centerY); 379 | mCamera.rotateX(rotation); 380 | mCamera.translate(0, 0, -centerY); 381 | 382 | // create the matrix if we haven't before 383 | if (mMatrix == null) { 384 | mMatrix = new Matrix(); 385 | } 386 | 387 | // get the matrix from the camera and then restore the camera 388 | mCamera.getMatrix(mMatrix); 389 | mCamera.restore(); 390 | 391 | // translate and scale the matrix 392 | mMatrix.preTranslate(-centerX, -centerY); 393 | mMatrix.postScale(scale, scale); 394 | mMatrix.postTranslate(left + centerX, top + centerY); 395 | 396 | // create and initialize the paint object 397 | if (mPaint == null) { 398 | mPaint = new Paint(); 399 | mPaint.setAntiAlias(true); 400 | mPaint.setFilterBitmap(true); 401 | } 402 | 403 | // set the light 404 | if (mLightEnabled) { 405 | mPaint.setColorFilter(calculateLight(rotation)); 406 | } else { 407 | mPaint.setAlpha(0xFF - (int)(2 * Math.abs(rotation))); 408 | } 409 | 410 | // draw the bitmap 411 | canvas.drawBitmap(view, mMatrix, mPaint); 412 | } 413 | 414 | /** 415 | * Calculates the lighting of the item based on rotation. 416 | * 417 | * @param rotation The rotation of the item 418 | * @return A color filter to use 419 | */ 420 | private LightingColorFilter calculateLight(final float rotation) { 421 | final double cosRotation = Math.cos(Math.PI * rotation / 180); 422 | int intensity = AMBIENT_LIGHT + (int)(DIFFUSE_LIGHT * cosRotation); 423 | int highlightIntensity = (int)(SPECULAR_LIGHT * Math.pow(cosRotation, SHININESS)); 424 | 425 | if (intensity > MAX_INTENSITY) { 426 | intensity = MAX_INTENSITY; 427 | } 428 | if (highlightIntensity > MAX_INTENSITY) { 429 | highlightIntensity = MAX_INTENSITY; 430 | } 431 | 432 | final int light = Color.rgb(intensity, intensity, intensity); 433 | final int highlight = Color.rgb(highlightIntensity, highlightIntensity, highlightIntensity); 434 | 435 | return new LightingColorFilter(light, highlight); 436 | } 437 | 438 | /** 439 | * Sets and initializes all things that need to when we start a touch 440 | * gesture. 441 | * 442 | * @param event The down event 443 | */ 444 | private void startTouch(final MotionEvent event) { 445 | // save the start place 446 | mTouchStartX = (int)event.getX(); 447 | mTouchStartY = (int)event.getY(); 448 | mListTopStart = getChildTop(getChildAt(0)) - mListTopOffset; 449 | 450 | // start checking for a long press 451 | startLongPressCheck(); 452 | 453 | // we don't know if it's a click or a scroll yet, but until we know 454 | // assume it's a click 455 | mTouchState = TOUCH_STATE_CLICK; 456 | } 457 | 458 | /** 459 | * Resets and recycles all things that need to when we end a touch gesture 460 | */ 461 | private void endTouch() { 462 | // remove any existing check for longpress 463 | removeCallbacks(mLongPressRunnable); 464 | 465 | // reset touch state 466 | mTouchState = TOUCH_STATE_RESTING; 467 | } 468 | 469 | /** 470 | * Scrolls the list. Takes care of updating rotation (if enabled) and 471 | * snapping 472 | * 473 | * @param scrolledDistance The distance to scroll 474 | */ 475 | private void scrollList(final int scrolledDistance) { 476 | mListTop = mListTopStart + scrolledDistance; 477 | if (mRotationEnabled) { 478 | mListRotation = -(DEGREES_PER_SCREEN * mListTop) / getHeight(); 479 | } 480 | requestLayout(); 481 | } 482 | 483 | /** 484 | * Posts (and creates if necessary) a runnable that will when executed call 485 | * the long click listener 486 | */ 487 | private void startLongPressCheck() { 488 | // create the runnable if we haven't already 489 | if (mLongPressRunnable == null) { 490 | mLongPressRunnable = new Runnable() { 491 | public void run() { 492 | if (mTouchState == TOUCH_STATE_CLICK) { 493 | final int index = getContainingChildIndex(mTouchStartX, mTouchStartY); 494 | if (index != INVALID_INDEX) { 495 | longClickChild(index); 496 | } 497 | } 498 | } 499 | }; 500 | } 501 | 502 | // then post it with a delay 503 | postDelayed(mLongPressRunnable, ViewConfiguration.getLongPressTimeout()); 504 | } 505 | 506 | /** 507 | * Checks if the user has moved far enough for this to be a scroll and if 508 | * so, sets the list in scroll mode 509 | * 510 | * @param event The (move) event 511 | * @return true if scroll was started, false otherwise 512 | */ 513 | private boolean startScrollIfNeeded(final MotionEvent event) { 514 | final int xPos = (int)event.getX(); 515 | final int yPos = (int)event.getY(); 516 | if (xPos < mTouchStartX - TOUCH_SCROLL_THRESHOLD 517 | || xPos > mTouchStartX + TOUCH_SCROLL_THRESHOLD 518 | || yPos < mTouchStartY - TOUCH_SCROLL_THRESHOLD 519 | || yPos > mTouchStartY + TOUCH_SCROLL_THRESHOLD) { 520 | // we've moved far enough for this to be a scroll 521 | removeCallbacks(mLongPressRunnable); 522 | mTouchState = TOUCH_STATE_SCROLL; 523 | return true; 524 | } 525 | return false; 526 | } 527 | 528 | /** 529 | * Returns the index of the child that contains the coordinates given. 530 | * 531 | * @param x X-coordinate 532 | * @param y Y-coordinate 533 | * @return The index of the child that contains the coordinates. If no child 534 | * is found then it returns INVALID_INDEX 535 | */ 536 | private int getContainingChildIndex(final int x, final int y) { 537 | if (mRect == null) { 538 | mRect = new Rect(); 539 | } 540 | for (int index = 0; index < getChildCount(); index++) { 541 | getChildAt(index).getHitRect(mRect); 542 | if (mRect.contains(x, y)) { 543 | return index; 544 | } 545 | } 546 | return INVALID_INDEX; 547 | } 548 | 549 | /** 550 | * Calls the item click listener for the child with at the specified 551 | * coordinates 552 | * 553 | * @param x The x-coordinate 554 | * @param y The y-coordinate 555 | */ 556 | private void clickChildAt(final int x, final int y) { 557 | final int index = getContainingChildIndex(x, y); 558 | if (index != INVALID_INDEX) { 559 | final View itemView = getChildAt(index); 560 | final int position = mFirstItemPosition + index; 561 | final long id = mAdapter.getItemId(position); 562 | performItemClick(itemView, position, id); 563 | } 564 | } 565 | 566 | /** 567 | * Calls the item long click listener for the child with the specified index 568 | * 569 | * @param index Child index 570 | */ 571 | private void longClickChild(final int index) { 572 | final View itemView = getChildAt(index); 573 | final int position = mFirstItemPosition + index; 574 | final long id = mAdapter.getItemId(position); 575 | final OnItemLongClickListener listener = getOnItemLongClickListener(); 576 | if (listener != null) { 577 | listener.onItemLongClick(this, itemView, position, id); 578 | } 579 | } 580 | 581 | /** 582 | * Removes view that are outside of the visible part of the list. Will not 583 | * remove all views. 584 | * 585 | * @param offset Offset of the visible area 586 | */ 587 | private void removeNonVisibleViews(final int offset) { 588 | // We need to keep close track of the child count in this function. We 589 | // should never remove all the views, because if we do, we loose track 590 | // of were we are. 591 | int childCount = getChildCount(); 592 | 593 | // if we are not at the bottom of the list and have more than one child 594 | if (mLastItemPosition != mAdapter.getCount() - 1 && childCount > 1) { 595 | // check if we should remove any views in the top 596 | View firstChild = getChildAt(0); 597 | while (firstChild != null && getChildBottom(firstChild) + offset < 0) { 598 | // remove the top view 599 | removeViewInLayout(firstChild); 600 | childCount--; 601 | mCachedItemViews.addLast(firstChild); 602 | mFirstItemPosition++; 603 | 604 | // update the list offset (since we've removed the top child) 605 | mListTopOffset += getChildHeight(firstChild); 606 | 607 | // Continue to check the next child only if we have more than 608 | // one child left 609 | if (childCount > 1) { 610 | firstChild = getChildAt(0); 611 | } else { 612 | firstChild = null; 613 | } 614 | } 615 | } 616 | 617 | // if we are not at the top of the list and have more than one child 618 | if (mFirstItemPosition != 0 && childCount > 1) { 619 | // check if we should remove any views in the bottom 620 | View lastChild = getChildAt(childCount - 1); 621 | while (lastChild != null && getChildTop(lastChild) + offset > getHeight()) { 622 | // remove the bottom view 623 | removeViewInLayout(lastChild); 624 | childCount--; 625 | mCachedItemViews.addLast(lastChild); 626 | mLastItemPosition--; 627 | 628 | // Continue to check the next child only if we have more than 629 | // one child left 630 | if (childCount > 1) { 631 | lastChild = getChildAt(childCount - 1); 632 | } else { 633 | lastChild = null; 634 | } 635 | } 636 | } 637 | } 638 | 639 | /** 640 | * Fills the list with child-views 641 | * 642 | * @param offset Offset of the visible area 643 | */ 644 | private void fillList(final int offset) { 645 | final int bottomEdge = getChildBottom(getChildAt(getChildCount() - 1)); 646 | fillListDown(bottomEdge, offset); 647 | 648 | final int topEdge = getChildTop(getChildAt(0)); 649 | fillListUp(topEdge, offset); 650 | } 651 | 652 | /** 653 | * Starts at the bottom and adds children until we've passed the list bottom 654 | * 655 | * @param bottomEdge The bottom edge of the currently last child 656 | * @param offset Offset of the visible area 657 | */ 658 | private void fillListDown(int bottomEdge, final int offset) { 659 | while (bottomEdge + offset < getHeight() && mLastItemPosition < mAdapter.getCount() - 1) { 660 | mLastItemPosition++; 661 | final View newBottomchild = mAdapter.getView(mLastItemPosition, getCachedView(), this); 662 | addAndMeasureChild(newBottomchild, LAYOUT_MODE_BELOW); 663 | bottomEdge += getChildHeight(newBottomchild); 664 | } 665 | } 666 | 667 | /** 668 | * Starts at the top and adds children until we've passed the list top 669 | * 670 | * @param topEdge The top edge of the currently first child 671 | * @param offset Offset of the visible area 672 | */ 673 | private void fillListUp(int topEdge, final int offset) { 674 | while (topEdge + offset > 0 && mFirstItemPosition > 0) { 675 | mFirstItemPosition--; 676 | final View newTopCild = mAdapter.getView(mFirstItemPosition, getCachedView(), this); 677 | addAndMeasureChild(newTopCild, LAYOUT_MODE_ABOVE); 678 | final int childHeight = getChildHeight(newTopCild); 679 | topEdge -= childHeight; 680 | 681 | // update the list offset (since we added a view at the top) 682 | mListTopOffset -= childHeight; 683 | } 684 | } 685 | 686 | /** 687 | * Adds a view as a child view and takes care of measuring it 688 | * 689 | * @param child The view to add 690 | * @param layoutMode Either LAYOUT_MODE_ABOVE or LAYOUT_MODE_BELOW 691 | */ 692 | private void addAndMeasureChild(final View child, final int layoutMode) { 693 | LayoutParams params = child.getLayoutParams(); 694 | if (params == null) { 695 | params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 696 | } 697 | final int index = layoutMode == LAYOUT_MODE_ABOVE ? 0 : -1; 698 | child.setDrawingCacheEnabled(true); 699 | addViewInLayout(child, index, params, true); 700 | 701 | final int itemWidth = (int)(getWidth() * ITEM_WIDTH); 702 | child.measure(MeasureSpec.EXACTLY | itemWidth, MeasureSpec.UNSPECIFIED); 703 | } 704 | 705 | /** 706 | * Positions the children at the "correct" positions 707 | */ 708 | private void positionItems() { 709 | int top = mListTop + mListTopOffset; 710 | 711 | for (int index = 0; index < getChildCount(); index++) { 712 | final View child = getChildAt(index); 713 | 714 | final int width = child.getMeasuredWidth(); 715 | final int height = child.getMeasuredHeight(); 716 | final int left = (getWidth() - width) / 2; 717 | final int margin = getChildMargin(child); 718 | final int childTop = top + margin; 719 | 720 | child.layout(left, childTop, left + width, childTop + height); 721 | top += height + 2 * margin; 722 | } 723 | } 724 | 725 | /** 726 | * Checks if there is a cached view that can be used 727 | * 728 | * @return A cached view or, if none was found, null 729 | */ 730 | private View getCachedView() { 731 | if (mCachedItemViews.size() != 0) { 732 | return mCachedItemViews.removeFirst(); 733 | } 734 | return null; 735 | } 736 | 737 | /** 738 | * Returns the margin of the child view taking into account the 739 | * ITEM_VERTICAL_SPACE 740 | * 741 | * @param child The child view 742 | * @return The margin of the child view 743 | */ 744 | private int getChildMargin(final View child) { 745 | return (int)(child.getMeasuredHeight() * (ITEM_VERTICAL_SPACE - 1) / 2); 746 | } 747 | 748 | /** 749 | * Returns the top placement of the child view taking into account the 750 | * ITEM_VERTICAL_SPACE 751 | * 752 | * @param child The child view 753 | * @return The top placement of the child view 754 | */ 755 | private int getChildTop(final View child) { 756 | return child.getTop() - getChildMargin(child); 757 | } 758 | 759 | /** 760 | * Returns the bottom placement of the child view taking into account the 761 | * ITEM_VERTICAL_SPACE 762 | * 763 | * @param child The child view 764 | * @return The bottom placement of the child view 765 | */ 766 | private int getChildBottom(final View child) { 767 | return child.getBottom() + getChildMargin(child); 768 | } 769 | 770 | /** 771 | * Returns the height of the child view taking into account the 772 | * ITEM_VERTICAL_SPACE 773 | * 774 | * @param child The child view 775 | * @return The height of the child view 776 | */ 777 | private int getChildHeight(final View child) { 778 | return child.getMeasuredHeight() + 2 * getChildMargin(child); 779 | } 780 | } 781 | -------------------------------------------------------------------------------- /app/src/main/java/com/dodola/bubblecloudexample/TestActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2010, Sony Ericsson Mobile Communication AB. All rights reserved. 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, 5 | * are permitted provided that the following conditions are met: 6 | * 7 | * * Redistributions of source code must retain the above copyright notice, this 8 | * list of conditions and the following disclaimer. 9 | * * Redistributions in binary form must reproduce the above copyright notice, 10 | * this list of conditions and the following disclaimer in the documentation 11 | * and/or other materials provided with the distribution. 12 | * * Neither the name of the Sony Ericsson Mobile Communication AB nor the names 13 | * of its contributors may be used to endorse or promote products derived from 14 | * this software without specific prior written permission. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 19 | * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 20 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 24 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 25 | * OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | package com.dodola.bubblecloudexample; 29 | 30 | import android.app.Activity; 31 | import android.content.Context; 32 | import android.content.pm.PackageInfo; 33 | import android.os.Bundle; 34 | import android.view.LayoutInflater; 35 | import android.view.View; 36 | import android.view.ViewGroup; 37 | import android.widget.AdapterView; 38 | import android.widget.AdapterView.OnItemClickListener; 39 | import android.widget.ArrayAdapter; 40 | import android.widget.Toast; 41 | 42 | import com.dodola.bubblecloud.BubbleCloudView; 43 | import com.dodola.bubblecloud.utils.FileManagerImageLoader; 44 | import com.pkmmte.view.CircularImageView; 45 | 46 | import java.util.ArrayList; 47 | import java.util.List; 48 | 49 | 50 | public class TestActivity extends Activity { 51 | 52 | private BubbleCloudView mListView; 53 | 54 | 55 | @Override 56 | public void onCreate(final Bundle savedInstanceState) { 57 | super.onCreate(savedInstanceState); 58 | setContentView(R.layout.main); 59 | FileManagerImageLoader.prepare(this.getApplication()); 60 | 61 | final ArrayList contacts = createContactList(19); 62 | final MyAdapter adapter = new MyAdapter(this, contacts); 63 | 64 | mListView = (BubbleCloudView) findViewById(R.id.my_list); 65 | mListView.setAdapter(adapter); 66 | 67 | mListView.setOnItemClickListener(new OnItemClickListener() { 68 | public void onItemClick(final AdapterView parent, final View view, 69 | final int position, final long id) { 70 | final String message = "OnClick: " + contacts.get(position); 71 | Toast.makeText(TestActivity.this, message, Toast.LENGTH_SHORT).show(); 72 | } 73 | }); 74 | 75 | 76 | } 77 | 78 | private ArrayList createContactList(final int size) { 79 | final ArrayList contacts = new ArrayList<>(); 80 | List packages = getPackageManager().getInstalledPackages(0); 81 | for (int i = 0; i < size; i++) { 82 | final PackageInfo packageInfo = packages.get(i); 83 | contacts.add(packageInfo.applicationInfo.sourceDir); 84 | } 85 | return contacts; 86 | } 87 | 88 | private static class MyAdapter extends ArrayAdapter { 89 | 90 | public MyAdapter(final Context context, final ArrayList contacts) { 91 | super(context, 0, contacts); 92 | } 93 | 94 | @Override 95 | public View getView(final int position, final View convertView, final ViewGroup parent) { 96 | View view = convertView; 97 | if (view == null) { 98 | view = LayoutInflater.from(getContext()).inflate(R.layout.list_item, null); 99 | } 100 | final CircularImageView itemRound = (CircularImageView) view.findViewById(R.id.item_round); 101 | FileManagerImageLoader.getInstance().addTask(getItem(position), itemRound, null, 48, 48, false); 102 | return view; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/BubbleCloudView/5c388b2ff8e40f6113091c71c34c5fcb654bd70f/app/src/main/res/drawable/background.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/contact_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/BubbleCloudView/5c388b2ff8e40f6113091c71c34c5fcb654bd70f/app/src/main/res/drawable/contact_image.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/BubbleCloudView/5c388b2ff8e40f6113091c71c34c5fcb654bd70f/app/src/main/res/drawable/icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/item_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/BubbleCloudView/5c388b2ff8e40f6113091c71c34c5fcb654bd70f/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/BubbleCloudView/5c388b2ff8e40f6113091c71c34c5fcb654bd70f/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/BubbleCloudView/5c388b2ff8e40f6113091c71c34c5fcb654bd70f/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/BubbleCloudView/5c388b2ff8e40f6113091c71c34c5fcb654bd70f/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 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 | BubbleCloudExample 3 | 4 | Hello world! 5 | Settings 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /bubblecloud/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /bubblecloud/bubblecloud.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /bubblecloud/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | minSdkVersion 14 9 | targetSdkVersion 22 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | compile 'com.android.support:appcompat-v7:22.2.1' 24 | compile 'com.pkmmte.view:circularimageview:1.1'} 25 | -------------------------------------------------------------------------------- /bubblecloud/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/dodola/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 | -------------------------------------------------------------------------------- /bubblecloud/src/androidTest/java/com/dodola/bubblecloud/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.dodola.bubblecloud; 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 | } -------------------------------------------------------------------------------- /bubblecloud/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /bubblecloud/src/main/java/com/dodola/bubblecloud/BubbleCloudView.java: -------------------------------------------------------------------------------- 1 | package com.dodola.bubblecloud; 2 | 3 | import android.animation.ValueAnimator; 4 | import android.content.Context; 5 | import android.graphics.Rect; 6 | import android.util.AttributeSet; 7 | import android.util.Log; 8 | import android.view.MotionEvent; 9 | import android.view.View; 10 | import android.widget.Adapter; 11 | import android.widget.AdapterView; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Arrays; 15 | 16 | /** 17 | * Created by dodola on 15/7/23. 18 | */ 19 | public class BubbleCloudView extends AdapterView { 20 | 21 | private T mAdapter; 22 | 23 | private int lastX; 24 | private int lastY; 25 | private int deltaX; 26 | private int deltaY; 27 | private int scrollMoveX; 28 | private int scrollMoveY; 29 | private int scrollX; 30 | private int scrollY; 31 | 32 | 33 | private int mTouchState = TOUCH_STATE_RESTING; 34 | private static final int TOUCH_STATE_RESTING = 0; 35 | private static final int TOUCH_STATE_CLICK = 1; 36 | private static final int TOUCH_STATE_SCROLL = 2; 37 | private static final int INVALID_INDEX = -1; 38 | private static final int TOUCH_SCROLL_THRESHOLD = 10; 39 | 40 | private Rect mRect; 41 | private int screenW; 42 | private int screenH; 43 | private int centerW; 44 | private int centerH; 45 | private ArrayList hexCube; 46 | private ArrayList hexCubeOrtho = new ArrayList<>(); 47 | private ArrayList hexCubePolar = new ArrayList<>(); 48 | private ArrayList hexCubeSphere = new ArrayList<>(); 49 | private int sphereR; 50 | private int hexR; 51 | private int itemSize; 52 | private int edgeSize; 53 | private float animAlpha = 1; 54 | int scrollRangeX = 30; 55 | int scrollRangeY = 10; 56 | 57 | private void startTouch(final MotionEvent event) { 58 | lastX = (int) event.getX(); 59 | lastY = (int) event.getY(); 60 | deltaX = 0; 61 | deltaY = 0; 62 | scrollMoveX += deltaX; 63 | scrollMoveY += deltaY; 64 | scrollX = scrollMoveX; 65 | scrollY = scrollMoveY; 66 | mTouchState = TOUCH_STATE_CLICK; 67 | } 68 | 69 | private void endTouch() { 70 | int veloX = deltaX; 71 | int veloY = deltaY; 72 | final int distanceX = veloX * 10; 73 | final int distanceY = veloY * 10; 74 | 75 | ValueAnimator endAnim = ValueAnimator.ofFloat(0, 1); 76 | endAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 77 | @Override 78 | public void onAnimationUpdate(ValueAnimator animation) { 79 | 80 | scrollMoveX = scrollX; 81 | scrollMoveY = scrollY; 82 | int steps = 16; 83 | int step = (int) (steps * animation.getAnimatedFraction()); 84 | 85 | int inertiaX = (int) (easeOutCubic 86 | (step, 0, distanceX, steps) - easeOutCubic((step - 1), 0, distanceX, steps)); 87 | int inertiaY = (int) (easeOutCubic 88 | (step, 0, distanceY, steps) - easeOutCubic((step - 1), 0, distanceY, steps)); 89 | 90 | scrollX += inertiaX; 91 | scrollY += inertiaY; 92 | 93 | if (scrollX > scrollRangeX) { 94 | scrollX -= (scrollX - scrollRangeX) / 4; 95 | } else if (scrollX < -scrollRangeX) { 96 | scrollX -= (scrollX + scrollRangeX) / 4; 97 | } 98 | 99 | if (scrollY > scrollRangeY) { 100 | scrollY -= (scrollY - scrollRangeY) / 4; 101 | } else if (scrollY < -scrollRangeY) { 102 | scrollY -= (scrollY + scrollRangeY) / 4; 103 | } 104 | iconMapRefresh(sphereR, hexR, scrollX, scrollY); 105 | requestLayout(); 106 | 107 | } 108 | }); 109 | endAnim.setDuration(300); 110 | endAnim.start(); 111 | 112 | mTouchState = TOUCH_STATE_RESTING; 113 | } 114 | 115 | 116 | public BubbleCloudView(Context context) { 117 | super(context); 118 | init(); 119 | } 120 | 121 | public BubbleCloudView(Context context, AttributeSet attrs) { 122 | super(context, attrs); 123 | init(); 124 | } 125 | 126 | public BubbleCloudView(Context context, AttributeSet attrs, int defStyleAttr) { 127 | super(context, attrs, defStyleAttr); 128 | init(); 129 | } 130 | 131 | @Override 132 | public T getAdapter() { 133 | return mAdapter; 134 | } 135 | 136 | @Override 137 | public void setAdapter(T adapter) { 138 | mAdapter = adapter; 139 | removeAllViewsInLayout(); 140 | requestLayout(); 141 | } 142 | 143 | @Override 144 | public View getSelectedView() { 145 | return null; 146 | 147 | } 148 | 149 | @Override 150 | public void setSelection(int position) { 151 | 152 | } 153 | 154 | @Override 155 | public boolean onInterceptTouchEvent(final MotionEvent event) { 156 | switch (event.getAction()) { 157 | case MotionEvent.ACTION_DOWN: 158 | startTouch(event); 159 | return false; 160 | 161 | case MotionEvent.ACTION_MOVE: 162 | return startScrollIfNeeded(event); 163 | 164 | default: 165 | endTouch(); 166 | return false; 167 | } 168 | } 169 | 170 | @Override 171 | public boolean onTouchEvent(MotionEvent event) { 172 | switch (event.getAction()) { 173 | case MotionEvent.ACTION_DOWN: 174 | startTouch(event); 175 | break; 176 | 177 | case MotionEvent.ACTION_MOVE: 178 | Log.d("BubbleCloudView", "===ActionMove==="); 179 | 180 | if (mTouchState == TOUCH_STATE_CLICK) { 181 | startScrollIfNeeded(event); 182 | } 183 | if (mTouchState == TOUCH_STATE_SCROLL) { 184 | scrollContainer((int) event.getX(), (int) event.getY()); 185 | Log.d("BubbleCloudView", "===mTouchScroll==="); 186 | } 187 | break; 188 | 189 | case MotionEvent.ACTION_UP: 190 | case MotionEvent.ACTION_CANCEL: 191 | if (mTouchState == TOUCH_STATE_CLICK) { 192 | clickChildAt((int) event.getX(), (int) event.getY()); 193 | } 194 | endTouch(); 195 | break; 196 | 197 | default: 198 | endTouch(); 199 | break; 200 | } 201 | return true; 202 | 203 | } 204 | 205 | private boolean startScrollIfNeeded(final MotionEvent event) { 206 | final int xPos = (int) event.getX(); 207 | final int yPos = (int) event.getY(); 208 | if (xPos < lastX - TOUCH_SCROLL_THRESHOLD 209 | || xPos > lastX + TOUCH_SCROLL_THRESHOLD 210 | || yPos < lastY - TOUCH_SCROLL_THRESHOLD 211 | || yPos > lastY + TOUCH_SCROLL_THRESHOLD) { 212 | mTouchState = TOUCH_STATE_SCROLL; 213 | return true; 214 | } 215 | return false; 216 | } 217 | 218 | private void scrollContainer(int x, int y) { 219 | 220 | 221 | deltaX = x - lastX; 222 | deltaY = y - lastY; 223 | 224 | lastX = x; 225 | lastY = y; 226 | 227 | scrollMoveX += deltaX; 228 | scrollMoveY += deltaY; 229 | 230 | scrollX = scrollMoveX; 231 | scrollY = scrollMoveY; 232 | 233 | 234 | if (scrollMoveX > scrollRangeX) { 235 | scrollX = scrollRangeX + (scrollMoveX - scrollRangeX) / 2; 236 | } else if (scrollX < -scrollRangeX) { 237 | scrollX = -scrollRangeX + (scrollMoveX + scrollRangeX) / 2; 238 | } 239 | 240 | if (scrollMoveY > scrollRangeY) { 241 | scrollY = scrollRangeY + (scrollMoveY - scrollRangeY) / 2; 242 | } else if (scrollY < -scrollRangeY) { 243 | scrollY = -scrollRangeY + (scrollMoveY + scrollRangeY) / 2; 244 | } 245 | 246 | 247 | iconMapRefresh(sphereR, hexR, 248 | scrollX, 249 | scrollY 250 | ); 251 | requestLayout(); 252 | } 253 | 254 | private void clickChildAt(final int x, final int y) { 255 | final int index = getContainingChildIndex(x, y); 256 | if (index != INVALID_INDEX) { 257 | final View itemView = getChildAt(index); 258 | final int position = index; 259 | final long id = mAdapter.getItemId(position); 260 | performItemClick(itemView, position, id); 261 | } 262 | } 263 | 264 | private int getContainingChildIndex(final int x, final int y) { 265 | if (mRect == null) { 266 | mRect = new Rect(); 267 | } 268 | for (int index = 0; index < getChildCount(); index++) { 269 | getChildAt(index).getHitRect(mRect); 270 | if (mRect.contains(x, y)) { 271 | return index; 272 | } 273 | } 274 | return INVALID_INDEX; 275 | } 276 | 277 | @Override 278 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 279 | super.onLayout(changed, left, top, right, bottom); 280 | 281 | if (mAdapter == null) { 282 | return; 283 | } 284 | 285 | if (getChildCount() == 0) { 286 | int position = 0; 287 | while (position < mAdapter.getCount()) { 288 | View newBottomChild = mAdapter.getView(position, null, this); 289 | addAndMeasureChild(newBottomChild); 290 | position++; 291 | } 292 | } 293 | 294 | positionItems(); 295 | } 296 | 297 | private void addAndMeasureChild(View child) { 298 | LayoutParams params = child.getLayoutParams(); 299 | if (params == null) { 300 | params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 301 | } 302 | addViewInLayout(child, -1, params, true); 303 | child.measure(MeasureSpec.EXACTLY | itemSize, MeasureSpec.EXACTLY | itemSize); 304 | } 305 | 306 | private void positionItems() { 307 | 308 | for (int index = 0; index < getChildCount(); index++) { 309 | View child = getChildAt(index); 310 | 311 | final XY xy = hexCubeOrtho.get(index); 312 | 313 | int width = child.getMeasuredWidth(); 314 | int height = child.getMeasuredHeight(); 315 | int offsetX = centerW - edgeSize; 316 | int offsetY = centerH - edgeSize; 317 | 318 | child.layout((int) xy.x + offsetX, (int) xy.y + offsetY, (int) xy.x + offsetX + width, (int) xy.y + offsetY + height); 319 | child.setScaleX(xy.scale); 320 | child.setScaleY(xy.scale); 321 | child.setAlpha(animAlpha); 322 | } 323 | } 324 | 325 | private void init() { 326 | 327 | this.hexCube = new ArrayList<>(); 328 | for (int i = 0; i < 4; i++) 329 | for (int j = -i; j <= i; j++) 330 | for (int k = -i; k <= i; k++) 331 | for (int l = -i; l <= i; l++) 332 | if (Math.abs(j) + Math.abs(k) + Math.abs(l) == i * 2 && j + k + l == 0) { 333 | final Integer[] integers = {j, k, l}; 334 | this.hexCube.add(integers); 335 | Log.d("BubbleCloudView", "hexCube:" + Arrays.deepToString(integers)); 336 | } 337 | 338 | this.screenW = getResources().getDimensionPixelSize(R.dimen.screenw); 339 | this.screenH = getResources().getDimensionPixelSize(R.dimen.screenh); 340 | this.sphereR = getResources().getDimensionPixelSize(R.dimen.sphereR); 341 | this.hexR = getResources().getDimensionPixelSize(R.dimen.hexR); 342 | this.itemSize = getResources().getDimensionPixelSize(R.dimen.item_size); 343 | this.edgeSize = getResources().getDimensionPixelSize(R.dimen.edge_size); 344 | this.centerW = this.screenW / 2; 345 | this.centerH = this.screenH / 2; 346 | iconMapRefresh(sphereR, hexR + 100, 347 | 0, 348 | 0); 349 | } 350 | 351 | @Override 352 | protected void onAttachedToWindow() { 353 | super.onAttachedToWindow(); 354 | //开启动画 355 | startEnterAnim(); 356 | } 357 | 358 | private void iconMapRefresh(float sphereR, float hexR, float scrollX, float scrollY) { 359 | // Log.d("BubbleCloudView", "sphereR:" + sphereR + ",hexR:" + hexR + ",scrollX:" + scrollX + ",scrollY:" + scrollY); 360 | 361 | hexCubeOrtho.clear(); 362 | hexCubePolar.clear(); 363 | hexCubeSphere.clear(); 364 | 365 | for (int i = 0; i < hexCube.size(); i++) { 366 | final Integer[] integers = this.hexCube.get(i); 367 | XY tempxy = new XY(); 368 | tempxy.x = (integers[1] + integers[0] / 2f) * hexR + scrollX; 369 | tempxy.y = (float) (Math.sqrt(3) / 2 * integers[0] * hexR + scrollY); 370 | hexCubeOrtho.add(tempxy); 371 | } 372 | 373 | for (int i = 0; i < hexCubeOrtho.size(); i++) { 374 | final XY hexCubexy = hexCubeOrtho.get(i); 375 | hexCubePolar.add(ortho2polar(hexCubexy.x, hexCubexy.y)); 376 | } 377 | 378 | 379 | for (int i = 0; i < hexCubePolar.size(); i++) { 380 | 381 | final Rrad rrad = hexCubePolar.get(i); 382 | 383 | float rad = rrad.r / sphereR; 384 | float r, deepth; 385 | if (rad < Math.PI / 2) { 386 | r = rrad.r * swing((float) (rad / (Math.PI / 2)), 1.5f, -0.5f, 1f); 387 | deepth = easeInOutCubic((float) (rad / (Math.PI / 2)), 1f, -0.5f, 1f); 388 | } else { 389 | r = rrad.r; 390 | deepth = easeInOutCubic(1f, 1f, -0.5f, 1f); 391 | } 392 | 393 | Rdr rdr = new Rdr(); 394 | rdr.r = r; 395 | rdr.deepth = deepth; 396 | rdr.rad = rrad.rad; 397 | hexCubeSphere.add(rdr); 398 | } 399 | // Log.d("BubbleCloudView", "iconMapRefresh:resultMap1====" + hexCubeSphere); 400 | 401 | hexCubeOrtho.clear(); 402 | for (int i = 0; i < hexCubeSphere.size(); i++) { 403 | final Rdr rdr = hexCubeSphere.get(i); 404 | hexCubeOrtho.add(polar2ortho(rdr.r, rdr.rad)); 405 | } 406 | 407 | for (int i = 0; i < hexCubeOrtho.size(); i++) { 408 | final XY xy = hexCubeOrtho.get(i); 409 | xy.x = Math.round(xy.x * 10) / 10; 410 | xy.y = (float) (Math.round(xy.y * 10) / 10 * 1.14); 411 | } 412 | 413 | 414 | final float edge = edgeSize; 415 | for (int i = 0; i < hexCubeOrtho.size(); i++) { 416 | final XY xy = hexCubeOrtho.get(i); 417 | final Rdr rdr = hexCubeSphere.get(i); 418 | if (Math.abs(xy.x) > this.screenW / 2 - edge || Math.abs(xy.y) > this.screenH / 2 - edge) { 419 | xy.scale = rdr.deepth * 0.4f; 420 | } else if (Math.abs(xy.x) > this.screenW / 2 - 2 * edge && Math.abs(xy.y) > this.screenH / 2 - 2 * edge) { 421 | xy.scale = Math.min(rdr.deepth * easeInOutSine(this.screenW / 2 - Math.abs(xy.x) - edge, 0.4f, 0.6f, edge), rdr.deepth * easeInOutSine(this.screenH / 2 - Math.abs(xy.y) - edge, 0.3f, 0.7f, edge)); 422 | } else if (Math.abs(xy.x) > this.screenW / 2 - 2 * edge) { 423 | xy.scale = rdr.deepth * easeOutSine(this.screenW / 2 - Math.abs(xy.x) - edge, 0.4f, 0.6f, edge); 424 | } else if (Math.abs(xy.y) > this.screenH / 2 - 2 * edge) { 425 | xy.scale = rdr.deepth * easeOutSine(this.screenH / 2 - Math.abs(xy.y) - edge, 0.4f, 0.6f, edge); 426 | } else { 427 | xy.scale = rdr.deepth; 428 | } 429 | } 430 | 431 | for (int i = 0; i < hexCubeOrtho.size(); i++) { 432 | final XY xy = hexCubeOrtho.get(i); 433 | if (xy.x < -this.screenW / 2 + 2 * edge) { 434 | xy.x += easeInSine(this.screenW / 2 - Math.abs(xy.x) - 2 * edge, 0, 6f, 2 * edge); 435 | } else if (xy.x > this.screenW / 2 - 2 * edge) { 436 | xy.x += easeInSine(this.screenW / 2 - Math.abs(xy.x) - 2 * edge, 0, -6f, 2 * edge); 437 | } 438 | if (xy.y < -this.screenH / 2 + 2 * edge) { 439 | xy.y += easeInSine(this.screenH / 2 - Math.abs(xy.y) - 2 * edge, 0, 8f, 2 * edge); 440 | } else if (xy.y > this.screenH / 2 - 2 * edge) { 441 | xy.y += easeInSine(this.screenH / 2 - Math.abs(xy.y) - 2 * edge, 0, -8f, 2 * edge); 442 | } 443 | } 444 | // Log.d("BubbleCloudView", "iconMapRefresh:resultMap2====" + hexCubeOrtho); 445 | 446 | } 447 | 448 | private float easeInSine(float t, float b, float c, float d) { 449 | return (float) (-c * Math.cos(t / d * (Math.PI / 2)) + c + b); 450 | } 451 | 452 | private float swing(float t, float b, float c, float d) { 453 | return -c * (t /= d) * (t - 2) + b; 454 | } 455 | 456 | private float easeInOutCubic(float t, float b, float c, float d) { 457 | if ((t /= d / 2) < 1) 458 | return c / 2 * t * t * t + b; 459 | return c / 2 * ((t -= 2) * t * t + 2) + b; 460 | } 461 | 462 | private float easeInOutSine(float t, float b, float c, float d) { 463 | return (float) (-c / 2 * (Math.cos(Math.PI * t / d) - 1) + b); 464 | } 465 | 466 | private float easeOutSine(float t, float b, float c, float d) { 467 | return (float) (c * Math.sin(t / d * (Math.PI / 2)) + b); 468 | } 469 | 470 | private XY polar2ortho(float r, float rad) { 471 | float x = (float) (r * Math.cos(rad)); 472 | float y = (float) (r * Math.sin(rad)); 473 | XY tempxy = new XY(); 474 | tempxy.x = x; 475 | tempxy.y = y; 476 | return tempxy; 477 | } 478 | 479 | private Rrad ortho2polar(float x, float y) { 480 | float r = (float) Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); 481 | float rad = (float) Math.atan2(y, x); 482 | Rrad temprad = new Rrad(); 483 | temprad.r = r; 484 | temprad.rad = rad; 485 | return temprad; 486 | } 487 | 488 | private void startEnterAnim() { 489 | ValueAnimator startAnim = ValueAnimator.ofFloat(0, 1); 490 | startAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 491 | @Override 492 | public void onAnimationUpdate(ValueAnimator animation) { 493 | final float v = easeOutCubic((float) animation.getAnimatedValue() * 36, hexR * 2, -hexR, 36f); 494 | iconMapRefresh(sphereR, v, 0, 0); 495 | animAlpha = animation.getAnimatedFraction(); 496 | requestLayout(); 497 | } 498 | }); 499 | startAnim.setDuration(1000); 500 | startAnim.start(); 501 | } 502 | 503 | private float easeOutCubic(float t, float b, float c, float d) { 504 | return c * ((t = t / d - 1) * t * t + 1) + b; 505 | } 506 | 507 | private class XY { 508 | float x, y, scale; 509 | 510 | @Override 511 | public String toString() { 512 | return "XY{" + 513 | "scale=" + scale + 514 | ", x=" + x + 515 | ", y=" + y + 516 | '}'; 517 | } 518 | } 519 | 520 | private class Rrad { 521 | float r, rad; 522 | 523 | @Override 524 | public String toString() { 525 | return "Rrad{" + 526 | "r=" + r + 527 | ", rad=" + rad + 528 | '}'; 529 | } 530 | } 531 | 532 | private class Rdr { 533 | float r, deepth, rad; 534 | 535 | @Override 536 | public String toString() { 537 | return "Rdr{" + 538 | "deepth=" + deepth + 539 | ", r=" + r + 540 | ", rad=" + rad + 541 | '}'; 542 | } 543 | } 544 | 545 | 546 | } 547 | -------------------------------------------------------------------------------- /bubblecloud/src/main/java/com/dodola/bubblecloud/bitmapfun/AsyncTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2008 The Android Open Source Project 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.dodola.bubblecloud.bitmapfun; 18 | 19 | import android.annotation.TargetApi; 20 | import android.os.Handler; 21 | import android.os.Message; 22 | import android.os.Process; 23 | 24 | import java.util.ArrayDeque; 25 | import java.util.concurrent.BlockingQueue; 26 | import java.util.concurrent.Callable; 27 | import java.util.concurrent.CancellationException; 28 | import java.util.concurrent.ExecutionException; 29 | import java.util.concurrent.Executor; 30 | import java.util.concurrent.Executors; 31 | import java.util.concurrent.FutureTask; 32 | import java.util.concurrent.LinkedBlockingQueue; 33 | import java.util.concurrent.ThreadFactory; 34 | import java.util.concurrent.ThreadPoolExecutor; 35 | import java.util.concurrent.TimeUnit; 36 | import java.util.concurrent.TimeoutException; 37 | import java.util.concurrent.atomic.AtomicBoolean; 38 | import java.util.concurrent.atomic.AtomicInteger; 39 | 40 | /** 41 | * ************************************* 42 | * Copied from JB release framework: 43 | * https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/os/AsyncTask.java 44 | * 45 | * so that threading behavior on all OS versions is the same and we can tweak behavior by using 46 | * executeOnExecutor() if needed. 47 | * 48 | * There are 3 changes in this copy of AsyncTask: 49 | * -pre-HC a single thread executor is used for serial operation 50 | * (Executors.newSingleThreadExecutor) and is the default 51 | * -the default THREAD_POOL_EXECUTOR was changed to use DiscardOldestPolicy 52 | * -a new fixed thread pool called DUAL_THREAD_EXECUTOR was added 53 | * ************************************* 54 | * 55 | *

AsyncTask enables proper and easy use of the UI thread. This class allows to 56 | * perform background operations and publish results on the UI thread without 57 | * having to manipulate threads and/or handlers.

58 | * 59 | *

AsyncTask is designed to be a helper class around {@link Thread} and {@link Handler} 60 | * and does not constitute a generic threading framework. AsyncTasks should ideally be 61 | * used for short operations (a few seconds at the most.) If you need to keep threads 62 | * running for long periods of time, it is highly recommended you use the various APIs 63 | * provided by the java.util.concurrent pacakge such as {@link Executor}, 64 | * {@link ThreadPoolExecutor} and {@link FutureTask}.

65 | * 66 | *

An asynchronous task is defined by a computation that runs on a background thread and 67 | * whose result is published on the UI thread. An asynchronous task is defined by 3 generic 68 | * types, called Params, Progress and Result, 69 | * and 4 steps, called onPreExecute, doInBackground, 70 | * onProgressUpdate and onPostExecute.

71 | * 72 | *
73 | *

Developer Guides

74 | *

For more information about using tasks and threads, read the 75 | * Processes and 76 | * Threads developer guide.

77 | *
78 | * 79 | *

Usage

80 | *

AsyncTask must be subclassed to be used. The subclass will override at least 81 | * one method ({@link #doInBackground}), and most often will override a 82 | * second one ({@link #onPostExecute}.)

83 | * 84 | *

Here is an example of subclassing:

85 | *
 86 |  * private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
 87 |  *     protected Long doInBackground(URL... urls) {
 88 |  *         int count = urls.length;
 89 |  *         long totalSize = 0;
 90 |  *         for (int i = 0; i < count; i++) {
 91 |  *             totalSize += Downloader.downloadFile(urls[i]);
 92 |  *             publishProgress((int) ((i / (float) count) * 100));
 93 |  *             // Escape early if cancel() is called
 94 |  *             if (isCancelled()) break;
 95 |  *         }
 96 |  *         return totalSize;
 97 |  *     }
 98 |  *
 99 |  *     protected void onProgressUpdate(Integer... progress) {
100 |  *         setProgressPercent(progress[0]);
101 |  *     }
102 |  *
103 |  *     protected void onPostExecute(Long result) {
104 |  *         showDialog("Downloaded " + result + " bytes");
105 |  *     }
106 |  * }
107 |  * 
108 | * 109 | *

Once created, a task is executed very simply:

110 | *
111 |  * new DownloadFilesTask().execute(url1, url2, url3);
112 |  * 
113 | * 114 | *

AsyncTask's generic types

115 | *

The three types used by an asynchronous task are the following:

116 | *
    117 | *
  1. Params, the type of the parameters sent to the task upon 118 | * execution.
  2. 119 | *
  3. Progress, the type of the progress units published during 120 | * the background computation.
  4. 121 | *
  5. Result, the type of the result of the background 122 | * computation.
  6. 123 | *
124 | *

Not all types are always used by an asynchronous task. To mark a type as unused, 125 | * simply use the type {@link Void}:

126 | *
127 |  * private class MyTask extends AsyncTask<Void, Void, Void> { ... }
128 |  * 
129 | * 130 | *

The 4 steps

131 | *

When an asynchronous task is executed, the task goes through 4 steps:

132 | *
    133 | *
  1. {@link #onPreExecute()}, invoked on the UI thread immediately after the task 134 | * is executed. This step is normally used to setup the task, for instance by 135 | * showing a progress bar in the user interface.
  2. 136 | *
  3. {@link #doInBackground}, invoked on the background thread 137 | * immediately after {@link #onPreExecute()} finishes executing. This step is used 138 | * to perform background computation that can take a long time. The parameters 139 | * of the asynchronous task are passed to this step. The result of the computation must 140 | * be returned by this step and will be passed back to the last step. This step 141 | * can also use {@link #publishProgress} to publish one or more units 142 | * of progress. These values are published on the UI thread, in the 143 | * {@link #onProgressUpdate} step.
  4. 144 | *
  5. {@link #onProgressUpdate}, invoked on the UI thread after a 145 | * call to {@link #publishProgress}. The timing of the execution is 146 | * undefined. This method is used to display any form of progress in the user 147 | * interface while the background computation is still executing. For instance, 148 | * it can be used to animate a progress bar or show logs in a text field.
  6. 149 | *
  7. {@link #onPostExecute}, invoked on the UI thread after the background 150 | * computation finishes. The result of the background computation is passed to 151 | * this step as a parameter.
  8. 152 | *
153 | * 154 | *

Cancelling a task

155 | *

A task can be cancelled at any time by invoking {@link #cancel(boolean)}. Invoking 156 | * this method will cause subsequent calls to {@link #isCancelled()} to return true. 157 | * After invoking this method, {@link #onCancelled(Object)}, instead of 158 | * {@link #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])} 159 | * returns. To ensure that a task is cancelled as quickly as possible, you should always 160 | * check the return value of {@link #isCancelled()} periodically from 161 | * {@link #doInBackground(Object[])}, if possible (inside a loop for instance.)

162 | * 163 | *

Threading rules

164 | *

There are a few threading rules that must be followed for this class to 165 | * work properly:

166 | *
    167 | *
  • The AsyncTask class must be loaded on the UI thread. This is done 168 | * automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.
  • 169 | *
  • The task instance must be created on the UI thread.
  • 170 | *
  • {@link #execute} must be invoked on the UI thread.
  • 171 | *
  • Do not call {@link #onPreExecute()}, {@link #onPostExecute}, 172 | * {@link #doInBackground}, {@link #onProgressUpdate} manually.
  • 173 | *
  • The task can be executed only once (an exception will be thrown if 174 | * a second execution is attempted.)
  • 175 | *
176 | * 177 | *

Memory observability

178 | *

AsyncTask guarantees that all callback calls are synchronized in such a way that the following 179 | * operations are safe without explicit synchronizations.

180 | *
    181 | *
  • Set member fields in the constructor or {@link #onPreExecute}, and refer to them 182 | * in {@link #doInBackground}. 183 | *
  • Set member fields in {@link #doInBackground}, and refer to them in 184 | * {@link #onProgressUpdate} and {@link #onPostExecute}. 185 | *
186 | * 187 | *

Order of execution

188 | *

When first introduced, AsyncTasks were executed serially on a single background 189 | * thread. Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed 190 | * to a pool of threads allowing multiple tasks to operate in parallel. Starting with 191 | * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are executed on a single 192 | * thread to avoid common application errors caused by parallel execution.

193 | *

If you truly want parallel execution, you can invoke 194 | * {@link #executeOnExecutor(Executor, Object[])} with 195 | * {@link #THREAD_POOL_EXECUTOR}.

196 | */ 197 | public abstract class AsyncTask { 198 | private static final String LOG_TAG = "AsyncTask"; 199 | 200 | private static final int CORE_POOL_SIZE = 5; 201 | private static final int MAXIMUM_POOL_SIZE = 128; 202 | private static final int KEEP_ALIVE = 1; 203 | 204 | private static final ThreadFactory sThreadFactory = new ThreadFactory() { 205 | private final AtomicInteger mCount = new AtomicInteger(1); 206 | 207 | public Thread newThread(Runnable r) { 208 | return new Thread(r, "AsyncTask #" + mCount.getAndIncrement()); 209 | } 210 | }; 211 | 212 | private static final BlockingQueue sPoolWorkQueue = 213 | new LinkedBlockingQueue(10); 214 | 215 | /** 216 | * An {@link Executor} that can be used to execute tasks in parallel. 217 | */ 218 | public static final Executor THREAD_POOL_EXECUTOR 219 | = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, 220 | TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory, 221 | new ThreadPoolExecutor.DiscardOldestPolicy()); 222 | 223 | /** 224 | * An {@link Executor} that executes tasks one at a time in serial 225 | * order. This serialization is global to a particular process. 226 | */ 227 | public static final Executor SERIAL_EXECUTOR = Utils.hasHoneycomb() ? new SerialExecutor() : 228 | Executors.newSingleThreadExecutor(sThreadFactory); 229 | 230 | public static final Executor DUAL_THREAD_EXECUTOR = 231 | Executors.newFixedThreadPool(2, sThreadFactory); 232 | 233 | private static final int MESSAGE_POST_RESULT = 0x1; 234 | private static final int MESSAGE_POST_PROGRESS = 0x2; 235 | 236 | private static final InternalHandler sHandler = new InternalHandler(); 237 | 238 | private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; 239 | private final WorkerRunnable mWorker; 240 | private final FutureTask mFuture; 241 | 242 | private volatile Status mStatus = Status.PENDING; 243 | 244 | private final AtomicBoolean mCancelled = new AtomicBoolean(); 245 | private final AtomicBoolean mTaskInvoked = new AtomicBoolean(); 246 | 247 | @TargetApi(11) 248 | private static class SerialExecutor implements Executor { 249 | final ArrayDeque mTasks = new ArrayDeque(); 250 | Runnable mActive; 251 | 252 | public synchronized void execute(final Runnable r) { 253 | mTasks.offer(new Runnable() { 254 | public void run() { 255 | try { 256 | r.run(); 257 | } finally { 258 | scheduleNext(); 259 | } 260 | } 261 | }); 262 | if (mActive == null) { 263 | scheduleNext(); 264 | } 265 | } 266 | 267 | protected synchronized void scheduleNext() { 268 | if ((mActive = mTasks.poll()) != null) { 269 | THREAD_POOL_EXECUTOR.execute(mActive); 270 | } 271 | } 272 | } 273 | 274 | /** 275 | * Indicates the current status of the task. Each status will be set only once 276 | * during the lifetime of a task. 277 | */ 278 | public enum Status { 279 | /** 280 | * Indicates that the task has not been executed yet. 281 | */ 282 | PENDING, 283 | /** 284 | * Indicates that the task is running. 285 | */ 286 | RUNNING, 287 | /** 288 | * Indicates that {@link AsyncTask#onPostExecute} has finished. 289 | */ 290 | FINISHED, 291 | } 292 | 293 | /** @hide Used to force static handler to be created. */ 294 | public static void init() { 295 | sHandler.getLooper(); 296 | } 297 | 298 | /** @hide */ 299 | public static void setDefaultExecutor(Executor exec) { 300 | sDefaultExecutor = exec; 301 | } 302 | 303 | /** 304 | * Creates a new asynchronous task. This constructor must be invoked on the UI thread. 305 | */ 306 | public AsyncTask() { 307 | mWorker = new WorkerRunnable() { 308 | public Result call() throws Exception { 309 | mTaskInvoked.set(true); 310 | 311 | Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 312 | //noinspection unchecked 313 | return postResult(doInBackground(mParams)); 314 | } 315 | }; 316 | 317 | mFuture = new FutureTask(mWorker) { 318 | @Override 319 | protected void done() { 320 | try { 321 | postResultIfNotInvoked(get()); 322 | } catch (InterruptedException e) { 323 | android.util.Log.w(LOG_TAG, e); 324 | } catch (ExecutionException e) { 325 | throw new RuntimeException("An error occured while executing doInBackground()", 326 | e.getCause()); 327 | } catch (CancellationException e) { 328 | postResultIfNotInvoked(null); 329 | } 330 | } 331 | }; 332 | } 333 | 334 | private void postResultIfNotInvoked(Result result) { 335 | final boolean wasTaskInvoked = mTaskInvoked.get(); 336 | if (!wasTaskInvoked) { 337 | postResult(result); 338 | } 339 | } 340 | 341 | private Result postResult(Result result) { 342 | @SuppressWarnings("unchecked") 343 | Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT, 344 | new AsyncTaskResult(this, result)); 345 | message.sendToTarget(); 346 | return result; 347 | } 348 | 349 | /** 350 | * Returns the current status of this task. 351 | * 352 | * @return The current status. 353 | */ 354 | public final Status getStatus() { 355 | return mStatus; 356 | } 357 | 358 | /** 359 | * Override this method to perform a computation on a background thread. The 360 | * specified parameters are the parameters passed to {@link #execute} 361 | * by the caller of this task. 362 | * 363 | * This method can call {@link #publishProgress} to publish updates 364 | * on the UI thread. 365 | * 366 | * @param params The parameters of the task. 367 | * 368 | * @return A result, defined by the subclass of this task. 369 | * 370 | * @see #onPreExecute() 371 | * @see #onPostExecute 372 | * @see #publishProgress 373 | */ 374 | protected abstract Result doInBackground(Params... params); 375 | 376 | /** 377 | * Runs on the UI thread before {@link #doInBackground}. 378 | * 379 | * @see #onPostExecute 380 | * @see #doInBackground 381 | */ 382 | protected void onPreExecute() { 383 | } 384 | 385 | /** 386 | *

Runs on the UI thread after {@link #doInBackground}. The 387 | * specified result is the value returned by {@link #doInBackground}.

388 | * 389 | *

This method won't be invoked if the task was cancelled.

390 | * 391 | * @param result The result of the operation computed by {@link #doInBackground}. 392 | * 393 | * @see #onPreExecute 394 | * @see #doInBackground 395 | * @see #onCancelled(Object) 396 | */ 397 | @SuppressWarnings({"UnusedDeclaration"}) 398 | protected void onPostExecute(Result result) { 399 | } 400 | 401 | /** 402 | * Runs on the UI thread after {@link #publishProgress} is invoked. 403 | * The specified values are the values passed to {@link #publishProgress}. 404 | * 405 | * @param values The values indicating progress. 406 | * 407 | * @see #publishProgress 408 | * @see #doInBackground 409 | */ 410 | @SuppressWarnings({"UnusedDeclaration"}) 411 | protected void onProgressUpdate(Progress... values) { 412 | } 413 | 414 | /** 415 | *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and 416 | * {@link #doInBackground(Object[])} has finished.

417 | * 418 | *

The default implementation simply invokes {@link #onCancelled()} and 419 | * ignores the result. If you write your own implementation, do not call 420 | * super.onCancelled(result).

421 | * 422 | * @param result The result, if any, computed in 423 | * {@link #doInBackground(Object[])}, can be null 424 | * 425 | * @see #cancel(boolean) 426 | * @see #isCancelled() 427 | */ 428 | @SuppressWarnings({"UnusedParameters"}) 429 | protected void onCancelled(Result result) { 430 | onCancelled(); 431 | } 432 | 433 | /** 434 | *

Applications should preferably override {@link #onCancelled(Object)}. 435 | * This method is invoked by the default implementation of 436 | * {@link #onCancelled(Object)}.

437 | * 438 | *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and 439 | * {@link #doInBackground(Object[])} has finished.

440 | * 441 | * @see #onCancelled(Object) 442 | * @see #cancel(boolean) 443 | * @see #isCancelled() 444 | */ 445 | protected void onCancelled() { 446 | } 447 | 448 | /** 449 | * Returns true if this task was cancelled before it completed 450 | * normally. If you are calling {@link #cancel(boolean)} on the task, 451 | * the value returned by this method should be checked periodically from 452 | * {@link #doInBackground(Object[])} to end the task as soon as possible. 453 | * 454 | * @return true if task was cancelled before it completed 455 | * 456 | * @see #cancel(boolean) 457 | */ 458 | public final boolean isCancelled() { 459 | return mCancelled.get(); 460 | } 461 | 462 | /** 463 | *

Attempts to cancel execution of this task. This attempt will 464 | * fail if the task has already completed, already been cancelled, 465 | * or could not be cancelled for some other reason. If successful, 466 | * and this task has not started when cancel is called, 467 | * this task should never run. If the task has already started, 468 | * then the mayInterruptIfRunning parameter determines 469 | * whether the thread executing this task should be interrupted in 470 | * an attempt to stop the task.

471 | * 472 | *

Calling this method will result in {@link #onCancelled(Object)} being 473 | * invoked on the UI thread after {@link #doInBackground(Object[])} 474 | * returns. Calling this method guarantees that {@link #onPostExecute(Object)} 475 | * is never invoked. After invoking this method, you should check the 476 | * value returned by {@link #isCancelled()} periodically from 477 | * {@link #doInBackground(Object[])} to finish the task as early as 478 | * possible.

479 | * 480 | * @param mayInterruptIfRunning true if the thread executing this 481 | * task should be interrupted; otherwise, in-progress tasks are allowed 482 | * to complete. 483 | * 484 | * @return false if the task could not be cancelled, 485 | * typically because it has already completed normally; 486 | * true otherwise 487 | * 488 | * @see #isCancelled() 489 | * @see #onCancelled(Object) 490 | */ 491 | public final boolean cancel(boolean mayInterruptIfRunning) { 492 | mCancelled.set(true); 493 | return mFuture.cancel(mayInterruptIfRunning); 494 | } 495 | 496 | /** 497 | * Waits if necessary for the computation to complete, and then 498 | * retrieves its result. 499 | * 500 | * @return The computed result. 501 | * 502 | * @throws CancellationException If the computation was cancelled. 503 | * @throws ExecutionException If the computation threw an exception. 504 | * @throws InterruptedException If the current thread was interrupted 505 | * while waiting. 506 | */ 507 | public final Result get() throws InterruptedException, ExecutionException { 508 | return mFuture.get(); 509 | } 510 | 511 | /** 512 | * Waits if necessary for at most the given time for the computation 513 | * to complete, and then retrieves its result. 514 | * 515 | * @param timeout Time to wait before cancelling the operation. 516 | * @param unit The time unit for the timeout. 517 | * 518 | * @return The computed result. 519 | * 520 | * @throws CancellationException If the computation was cancelled. 521 | * @throws ExecutionException If the computation threw an exception. 522 | * @throws InterruptedException If the current thread was interrupted 523 | * while waiting. 524 | * @throws TimeoutException If the wait timed out. 525 | */ 526 | public final Result get(long timeout, TimeUnit unit) throws InterruptedException, 527 | ExecutionException, TimeoutException { 528 | return mFuture.get(timeout, unit); 529 | } 530 | 531 | /** 532 | * Executes the task with the specified parameters. The task returns 533 | * itself (this) so that the caller can keep a reference to it. 534 | * 535 | *

Note: this function schedules the task on a queue for a single background 536 | * thread or pool of threads depending on the platform version. When first 537 | * introduced, AsyncTasks were executed serially on a single background thread. 538 | * Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed 539 | * to a pool of threads allowing multiple tasks to operate in parallel. Starting 540 | * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being 541 | * executed on a single thread to avoid common application errors caused 542 | * by parallel execution. If you truly want parallel execution, you can use 543 | * the {@link #executeOnExecutor} version of this method 544 | * with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings 545 | * on its use. 546 | * 547 | *

This method must be invoked on the UI thread. 548 | * 549 | * @param params The parameters of the task. 550 | * 551 | * @return This instance of AsyncTask. 552 | * 553 | * @throws IllegalStateException If {@link #getStatus()} returns either 554 | * {@link Status#RUNNING} or {@link Status#FINISHED}. 555 | * 556 | * @see #executeOnExecutor(Executor, Object[]) 557 | * @see #execute(Runnable) 558 | */ 559 | public final AsyncTask execute(Params... params) { 560 | return executeOnExecutor(sDefaultExecutor, params); 561 | } 562 | 563 | /** 564 | * Executes the task with the specified parameters. The task returns 565 | * itself (this) so that the caller can keep a reference to it. 566 | * 567 | *

This method is typically used with {@link #THREAD_POOL_EXECUTOR} to 568 | * allow multiple tasks to run in parallel on a pool of threads managed by 569 | * AsyncTask, however you can also use your own {@link Executor} for custom 570 | * behavior. 571 | * 572 | *

Warning: Allowing multiple tasks to run in parallel from 573 | * a thread pool is generally not what one wants, because the order 574 | * of their operation is not defined. For example, if these tasks are used 575 | * to modify any state in common (such as writing a file due to a button click), 576 | * there are no guarantees on the order of the modifications. 577 | * Without careful work it is possible in rare cases for the newer version 578 | * of the data to be over-written by an older one, leading to obscure data 579 | * loss and stability issues. Such changes are best 580 | * executed in serial; to guarantee such work is serialized regardless of 581 | * platform version you can use this function with {@link #SERIAL_EXECUTOR}. 582 | * 583 | *

This method must be invoked on the UI thread. 584 | * 585 | * @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a 586 | * convenient process-wide thread pool for tasks that are loosely coupled. 587 | * @param params The parameters of the task. 588 | * 589 | * @return This instance of AsyncTask. 590 | * 591 | * @throws IllegalStateException If {@link #getStatus()} returns either 592 | * {@link Status#RUNNING} or {@link Status#FINISHED}. 593 | * 594 | * @see #execute(Object[]) 595 | */ 596 | public final AsyncTask executeOnExecutor(Executor exec, 597 | Params... params) { 598 | if (mStatus != Status.PENDING) { 599 | switch (mStatus) { 600 | case RUNNING: 601 | throw new IllegalStateException("Cannot execute task:" 602 | + " the task is already running."); 603 | case FINISHED: 604 | throw new IllegalStateException("Cannot execute task:" 605 | + " the task has already been executed " 606 | + "(a task can be executed only once)"); 607 | } 608 | } 609 | 610 | mStatus = Status.RUNNING; 611 | 612 | onPreExecute(); 613 | 614 | mWorker.mParams = params; 615 | exec.execute(mFuture); 616 | 617 | return this; 618 | } 619 | 620 | /** 621 | * Convenience version of {@link #execute(Object...)} for use with 622 | * a simple Runnable object. See {@link #execute(Object[])} for more 623 | * information on the order of execution. 624 | * 625 | * @see #execute(Object[]) 626 | * @see #executeOnExecutor(Executor, Object[]) 627 | */ 628 | public static void execute(Runnable runnable) { 629 | sDefaultExecutor.execute(runnable); 630 | } 631 | 632 | /** 633 | * This method can be invoked from {@link #doInBackground} to 634 | * publish updates on the UI thread while the background computation is 635 | * still running. Each call to this method will trigger the execution of 636 | * {@link #onProgressUpdate} on the UI thread. 637 | * 638 | * {@link #onProgressUpdate} will note be called if the task has been 639 | * canceled. 640 | * 641 | * @param values The progress values to update the UI with. 642 | * 643 | * @see #onProgressUpdate 644 | * @see #doInBackground 645 | */ 646 | protected final void publishProgress(Progress... values) { 647 | if (!isCancelled()) { 648 | sHandler.obtainMessage(MESSAGE_POST_PROGRESS, 649 | new AsyncTaskResult(this, values)).sendToTarget(); 650 | } 651 | } 652 | 653 | private void finish(Result result) { 654 | if (isCancelled()) { 655 | onCancelled(result); 656 | } else { 657 | onPostExecute(result); 658 | } 659 | mStatus = Status.FINISHED; 660 | } 661 | 662 | private static class InternalHandler extends Handler { 663 | @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) 664 | @Override 665 | public void handleMessage(Message msg) { 666 | AsyncTaskResult result = (AsyncTaskResult) msg.obj; 667 | switch (msg.what) { 668 | case MESSAGE_POST_RESULT: 669 | // There is only one result 670 | result.mTask.finish(result.mData[0]); 671 | break; 672 | case MESSAGE_POST_PROGRESS: 673 | result.mTask.onProgressUpdate(result.mData); 674 | break; 675 | } 676 | } 677 | } 678 | 679 | private static abstract class WorkerRunnable implements Callable { 680 | Params[] mParams; 681 | } 682 | 683 | @SuppressWarnings({"RawUseOfParameterizedType"}) 684 | private static class AsyncTaskResult { 685 | final AsyncTask mTask; 686 | final Data[] mData; 687 | 688 | AsyncTaskResult(AsyncTask task, Data... data) { 689 | mTask = task; 690 | mData = data; 691 | } 692 | } 693 | } -------------------------------------------------------------------------------- /bubblecloud/src/main/java/com/dodola/bubblecloud/bitmapfun/ImageFetcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 The Android Open Source Project 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.dodola.bubblecloud.bitmapfun; 18 | 19 | import android.content.Context; 20 | import android.graphics.Bitmap; 21 | import android.net.ConnectivityManager; 22 | import android.net.NetworkInfo; 23 | import android.os.Build; 24 | import android.util.Log; 25 | import android.widget.Toast; 26 | 27 | 28 | import com.dodola.bubblecloud.BuildConfig; 29 | 30 | import java.io.BufferedInputStream; 31 | import java.io.BufferedOutputStream; 32 | import java.io.File; 33 | import java.io.FileDescriptor; 34 | import java.io.FileInputStream; 35 | import java.io.IOException; 36 | import java.io.OutputStream; 37 | import java.net.HttpURLConnection; 38 | import java.net.URL; 39 | 40 | /** 41 | * A simple subclass of {@link ImageResizer} that fetches and resizes images fetched from a URL. 42 | */ 43 | public class ImageFetcher extends ImageResizer { 44 | private static final String TAG = "ImageFetcher"; 45 | private static final int HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10MB 46 | private static final String HTTP_CACHE_DIR = "http"; 47 | private static final int IO_BUFFER_SIZE = 8 * 1024; 48 | 49 | private DiskLruCache mHttpDiskCache; 50 | private File mHttpCacheDir; 51 | private boolean mHttpDiskCacheStarting = true; 52 | private final Object mHttpDiskCacheLock = new Object(); 53 | private static final int DISK_CACHE_INDEX = 0; 54 | 55 | /** 56 | * Initialize providing a target image width and height for the processing images. 57 | * 58 | * @param context 59 | * @param imageWidth 60 | * @param imageHeight 61 | */ 62 | public ImageFetcher(Context context, int imageWidth, int imageHeight) { 63 | super(context, imageWidth, imageHeight); 64 | init(context); 65 | } 66 | 67 | /** 68 | * Initialize providing a single target image size (used for both width and height); 69 | * 70 | * @param context 71 | * @param imageSize 72 | */ 73 | public ImageFetcher(Context context, int imageSize) { 74 | super(context, imageSize); 75 | init(context); 76 | } 77 | 78 | private void init(Context context) { 79 | checkConnection(context); 80 | mHttpCacheDir = ImageCache.getDiskCacheDir(context, HTTP_CACHE_DIR); 81 | } 82 | 83 | @Override 84 | protected void initDiskCacheInternal() { 85 | super.initDiskCacheInternal(); 86 | initHttpDiskCache(); 87 | } 88 | 89 | private void initHttpDiskCache() { 90 | if (!mHttpCacheDir.exists()) { 91 | mHttpCacheDir.mkdirs(); 92 | } 93 | synchronized (mHttpDiskCacheLock) { 94 | if (ImageCache.getUsableSpace(mHttpCacheDir) > HTTP_CACHE_SIZE) { 95 | try { 96 | mHttpDiskCache = DiskLruCache.open(mHttpCacheDir, 1, 1, HTTP_CACHE_SIZE); 97 | if (BuildConfig.DEBUG) { 98 | Log.d(TAG, "HTTP cache initialized"); 99 | } 100 | } catch (IOException e) { 101 | mHttpDiskCache = null; 102 | } 103 | } 104 | mHttpDiskCacheStarting = false; 105 | mHttpDiskCacheLock.notifyAll(); 106 | } 107 | } 108 | 109 | @Override 110 | protected void clearCacheInternal() { 111 | super.clearCacheInternal(); 112 | synchronized (mHttpDiskCacheLock) { 113 | if (mHttpDiskCache != null && !mHttpDiskCache.isClosed()) { 114 | try { 115 | mHttpDiskCache.delete(); 116 | if (BuildConfig.DEBUG) { 117 | Log.d(TAG, "HTTP cache cleared"); 118 | } 119 | } catch (IOException e) { 120 | Log.e(TAG, "clearCacheInternal - " + e); 121 | } 122 | mHttpDiskCache = null; 123 | mHttpDiskCacheStarting = true; 124 | initHttpDiskCache(); 125 | } 126 | } 127 | } 128 | 129 | @Override 130 | protected void flushCacheInternal() { 131 | super.flushCacheInternal(); 132 | synchronized (mHttpDiskCacheLock) { 133 | if (mHttpDiskCache != null) { 134 | try { 135 | mHttpDiskCache.flush(); 136 | if (BuildConfig.DEBUG) { 137 | Log.d(TAG, "HTTP cache flushed"); 138 | } 139 | } catch (IOException e) { 140 | Log.e(TAG, "flush - " + e); 141 | } 142 | } 143 | } 144 | } 145 | 146 | @Override 147 | protected void closeCacheInternal() { 148 | super.closeCacheInternal(); 149 | synchronized (mHttpDiskCacheLock) { 150 | if (mHttpDiskCache != null) { 151 | try { 152 | if (!mHttpDiskCache.isClosed()) { 153 | mHttpDiskCache.close(); 154 | mHttpDiskCache = null; 155 | if (BuildConfig.DEBUG) { 156 | Log.d(TAG, "HTTP cache closed"); 157 | } 158 | } 159 | } catch (IOException e) { 160 | Log.e(TAG, "closeCacheInternal - " + e); 161 | } 162 | } 163 | } 164 | } 165 | 166 | /** 167 | * Simple network connection check. 168 | * 169 | * @param context 170 | */ 171 | private void checkConnection(Context context) { 172 | final ConnectivityManager cm = 173 | (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 174 | final NetworkInfo networkInfo = cm.getActiveNetworkInfo(); 175 | if (networkInfo == null || !networkInfo.isConnectedOrConnecting()) { 176 | Log.e(TAG, "checkConnection - no connection found"); 177 | } 178 | } 179 | 180 | /** 181 | * The main process method, which will be called by the ImageWorker in the AsyncTask background 182 | * thread. 183 | * 184 | * @param data The data to load the bitmap, in this case, a regular http URL 185 | * @return The downloaded and resized bitmap 186 | */ 187 | private Bitmap processBitmap(String data) { 188 | if (BuildConfig.DEBUG) { 189 | Log.d(TAG, "processBitmap - " + data); 190 | } 191 | 192 | final String key = ImageCache.hashKeyForDisk(data); 193 | FileDescriptor fileDescriptor = null; 194 | FileInputStream fileInputStream = null; 195 | DiskLruCache.Snapshot snapshot; 196 | synchronized (mHttpDiskCacheLock) { 197 | // Wait for disk cache to initialize 198 | while (mHttpDiskCacheStarting) { 199 | try { 200 | mHttpDiskCacheLock.wait(); 201 | } catch (InterruptedException e) { 202 | } 203 | } 204 | 205 | if (mHttpDiskCache != null) { 206 | try { 207 | snapshot = mHttpDiskCache.get(key); 208 | if (snapshot == null) { 209 | if (BuildConfig.DEBUG) { 210 | Log.d(TAG, "processBitmap, not found in http cache, downloading..."); 211 | } 212 | DiskLruCache.Editor editor = mHttpDiskCache.edit(key); 213 | if (editor != null) { 214 | if (downloadUrlToStream(data, 215 | editor.newOutputStream(DISK_CACHE_INDEX))) { 216 | editor.commit(); 217 | } else { 218 | editor.abort(); 219 | } 220 | } 221 | snapshot = mHttpDiskCache.get(key); 222 | } 223 | if (snapshot != null) { 224 | fileInputStream = 225 | (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX); 226 | fileDescriptor = fileInputStream.getFD(); 227 | } 228 | } catch (IOException e) { 229 | Log.e(TAG, "processBitmap - " + e); 230 | } catch (IllegalStateException e) { 231 | Log.e(TAG, "processBitmap - " + e); 232 | } finally { 233 | if (fileDescriptor == null && fileInputStream != null) { 234 | try { 235 | fileInputStream.close(); 236 | } catch (IOException e) { 237 | } 238 | } 239 | } 240 | } 241 | } 242 | 243 | Bitmap bitmap = null; 244 | if (fileDescriptor != null) { 245 | bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth, 246 | mImageHeight, getImageCache()); 247 | } 248 | if (fileInputStream != null) { 249 | try { 250 | fileInputStream.close(); 251 | } catch (IOException e) { 252 | } 253 | } 254 | return bitmap; 255 | } 256 | 257 | @Override 258 | protected Bitmap processBitmap(Object data) { 259 | return processBitmap(String.valueOf(data)); 260 | } 261 | 262 | /** 263 | * Download a bitmap from a URL and write the content to an output stream. 264 | * 265 | * @param urlString The URL to fetch 266 | * @return true if successful, false otherwise 267 | */ 268 | public boolean downloadUrlToStream(String urlString, OutputStream outputStream) { 269 | disableConnectionReuseIfNecessary(); 270 | HttpURLConnection urlConnection = null; 271 | BufferedOutputStream out = null; 272 | BufferedInputStream in = null; 273 | 274 | try { 275 | final URL url = new URL(urlString); 276 | urlConnection = (HttpURLConnection) url.openConnection(); 277 | in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE); 278 | out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE); 279 | 280 | int b; 281 | while ((b = in.read()) != -1) { 282 | out.write(b); 283 | } 284 | return true; 285 | } catch (final IOException e) { 286 | Log.e(TAG, "Error in downloadBitmap - " + e); 287 | } finally { 288 | if (urlConnection != null) { 289 | urlConnection.disconnect(); 290 | } 291 | try { 292 | if (out != null) { 293 | out.close(); 294 | } 295 | if (in != null) { 296 | in.close(); 297 | } 298 | } catch (final IOException e) { 299 | } 300 | } 301 | return false; 302 | } 303 | 304 | /** 305 | * Workaround for bug pre-Froyo, see here for more info: 306 | * http://android-developers.blogspot.com/2011/09/androids-http-clients.html 307 | */ 308 | public static void disableConnectionReuseIfNecessary() { 309 | // HTTP connection reuse which was buggy pre-froyo 310 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) { 311 | System.setProperty("http.keepAlive", "false"); 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /bubblecloud/src/main/java/com/dodola/bubblecloud/bitmapfun/ImageResizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 The Android Open Source Project 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.dodola.bubblecloud.bitmapfun; 18 | 19 | import android.annotation.TargetApi; 20 | import android.content.Context; 21 | import android.content.res.Resources; 22 | import android.graphics.Bitmap; 23 | import android.graphics.BitmapFactory; 24 | import android.os.Build; 25 | 26 | 27 | import java.io.FileDescriptor; 28 | 29 | /** 30 | * A simple subclass of {@link ImageWorker} that resizes images from resources given a target width 31 | * and height. Useful for when the input images might be too large to simply load directly into 32 | * memory. 33 | */ 34 | public class ImageResizer extends ImageWorker { 35 | private static final String TAG = "ImageResizer"; 36 | protected int mImageWidth; 37 | protected int mImageHeight; 38 | 39 | /** 40 | * Initialize providing a single target image size (used for both width and height); 41 | * 42 | * @param context 43 | * @param imageWidth 44 | * @param imageHeight 45 | */ 46 | public ImageResizer(Context context, int imageWidth, int imageHeight) { 47 | super(context); 48 | setImageSize(imageWidth, imageHeight); 49 | } 50 | 51 | /** 52 | * Initialize providing a single target image size (used for both width and height); 53 | * 54 | * @param context 55 | * @param imageSize 56 | */ 57 | public ImageResizer(Context context, int imageSize) { 58 | super(context); 59 | setImageSize(imageSize); 60 | } 61 | 62 | /** 63 | * Set the target image width and height. 64 | * 65 | * @param width 66 | * @param height 67 | */ 68 | public void setImageSize(int width, int height) { 69 | mImageWidth = width; 70 | mImageHeight = height; 71 | } 72 | 73 | /** 74 | * Set the target image size (width and height will be the same). 75 | * 76 | * @param size 77 | */ 78 | public void setImageSize(int size) { 79 | setImageSize(size, size); 80 | } 81 | 82 | /** 83 | * The main processing method. This happens in a background task. In this case we are just 84 | * sampling down the bitmap and returning it from a resource. 85 | * 86 | * @param resId 87 | * @return 88 | */ 89 | private Bitmap processBitmap(int resId) { 90 | return decodeSampledBitmapFromResource(mResources, resId, mImageWidth, 91 | mImageHeight, getImageCache()); 92 | } 93 | 94 | @Override 95 | protected Bitmap processBitmap(Object data) { 96 | return processBitmap(Integer.parseInt(String.valueOf(data))); 97 | } 98 | 99 | /** 100 | * Decode and sample down a bitmap from resources to the requested width and height. 101 | * 102 | * @param res The resources object containing the image data 103 | * @param resId The resource id of the image data 104 | * @param reqWidth The requested width of the resulting bitmap 105 | * @param reqHeight The requested height of the resulting bitmap 106 | * @param cache The ImageCache used to find candidate bitmaps for use with inBitmap 107 | * @return A bitmap sampled down from the original with the same aspect ratio and dimensions 108 | * that are equal to or greater than the requested width and height 109 | */ 110 | public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, 111 | int reqWidth, int reqHeight, ImageCache cache) { 112 | 113 | // BEGIN_INCLUDE (read_bitmap_dimensions) 114 | // First decode with inJustDecodeBounds=true to check dimensions 115 | final BitmapFactory.Options options = new BitmapFactory.Options(); 116 | options.inJustDecodeBounds = true; 117 | BitmapFactory.decodeResource(res, resId, options); 118 | 119 | // Calculate inSampleSize 120 | options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); 121 | // END_INCLUDE (read_bitmap_dimensions) 122 | 123 | // If we're running on Honeycomb or newer, try to use inBitmap 124 | if (Utils.hasHoneycomb()) { 125 | addInBitmapOptions(options, cache); 126 | } 127 | 128 | // Decode bitmap with inSampleSize set 129 | options.inJustDecodeBounds = false; 130 | return BitmapFactory.decodeResource(res, resId, options); 131 | } 132 | 133 | /** 134 | * Decode and sample down a bitmap from a file to the requested width and height. 135 | * 136 | * @param filename The full path of the file to decode 137 | * @param reqWidth The requested width of the resulting bitmap 138 | * @param reqHeight The requested height of the resulting bitmap 139 | * @param cache The ImageCache used to find candidate bitmaps for use with inBitmap 140 | * @return A bitmap sampled down from the original with the same aspect ratio and dimensions 141 | * that are equal to or greater than the requested width and height 142 | */ 143 | public static Bitmap decodeSampledBitmapFromFile(String filename, 144 | int reqWidth, int reqHeight, ImageCache cache) { 145 | 146 | // First decode with inJustDecodeBounds=true to check dimensions 147 | final BitmapFactory.Options options = new BitmapFactory.Options(); 148 | options.inJustDecodeBounds = true; 149 | BitmapFactory.decodeFile(filename, options); 150 | 151 | // Calculate inSampleSize 152 | options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); 153 | 154 | // If we're running on Honeycomb or newer, try to use inBitmap 155 | if (Utils.hasHoneycomb()) { 156 | addInBitmapOptions(options, cache); 157 | } 158 | 159 | // Decode bitmap with inSampleSize set 160 | options.inJustDecodeBounds = false; 161 | return BitmapFactory.decodeFile(filename, options); 162 | } 163 | 164 | /** 165 | * Decode and sample down a bitmap from a file input stream to the requested width and height. 166 | * 167 | * @param fileDescriptor The file descriptor to read from 168 | * @param reqWidth The requested width of the resulting bitmap 169 | * @param reqHeight The requested height of the resulting bitmap 170 | * @param cache The ImageCache used to find candidate bitmaps for use with inBitmap 171 | * @return A bitmap sampled down from the original with the same aspect ratio and dimensions 172 | * that are equal to or greater than the requested width and height 173 | */ 174 | public static Bitmap decodeSampledBitmapFromDescriptor( 175 | FileDescriptor fileDescriptor, int reqWidth, int reqHeight, ImageCache cache) { 176 | 177 | // First decode with inJustDecodeBounds=true to check dimensions 178 | final BitmapFactory.Options options = new BitmapFactory.Options(); 179 | options.inJustDecodeBounds = true; 180 | BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); 181 | 182 | // Calculate inSampleSize 183 | options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); 184 | 185 | // Decode bitmap with inSampleSize set 186 | options.inJustDecodeBounds = false; 187 | 188 | // If we're running on Honeycomb or newer, try to use inBitmap 189 | if (Utils.hasHoneycomb()) { 190 | addInBitmapOptions(options, cache); 191 | } 192 | 193 | return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); 194 | } 195 | 196 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 197 | private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) { 198 | //BEGIN_INCLUDE(add_bitmap_options) 199 | // inBitmap only works with mutable bitmaps so force the decoder to 200 | // return mutable bitmaps. 201 | options.inMutable = true; 202 | 203 | if (cache != null) { 204 | // Try and find a bitmap to use for inBitmap 205 | Bitmap inBitmap = cache.getBitmapFromReusableSet(options); 206 | 207 | if (inBitmap != null) { 208 | options.inBitmap = inBitmap; 209 | } 210 | } 211 | //END_INCLUDE(add_bitmap_options) 212 | } 213 | 214 | /** 215 | * Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding 216 | * bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates 217 | * the closest inSampleSize that is a power of 2 and will result in the final decoded bitmap 218 | * having a width and height equal to or larger than the requested width and height. 219 | * 220 | * @param options An options object with out* params already populated (run through a decode* 221 | * method with inJustDecodeBounds==true 222 | * @param reqWidth The requested width of the resulting bitmap 223 | * @param reqHeight The requested height of the resulting bitmap 224 | * @return The value to be used for inSampleSize 225 | */ 226 | public static int calculateInSampleSize(BitmapFactory.Options options, 227 | int reqWidth, int reqHeight) { 228 | // BEGIN_INCLUDE (calculate_sample_size) 229 | // Raw height and width of image 230 | final int height = options.outHeight; 231 | final int width = options.outWidth; 232 | int inSampleSize = 1; 233 | 234 | if (height > reqHeight || width > reqWidth) { 235 | 236 | final int halfHeight = height / 2; 237 | final int halfWidth = width / 2; 238 | 239 | // Calculate the largest inSampleSize value that is a power of 2 and keeps both 240 | // height and width larger than the requested height and width. 241 | while ((halfHeight / inSampleSize) > reqHeight 242 | && (halfWidth / inSampleSize) > reqWidth) { 243 | inSampleSize *= 2; 244 | } 245 | 246 | // This offers some additional logic in case the image has a strange 247 | // aspect ratio. For example, a panorama may have a much larger 248 | // width than height. In these cases the total pixels might still 249 | // end up being too large to fit comfortably in memory, so we should 250 | // be more aggressive with sample down the image (=larger inSampleSize). 251 | 252 | long totalPixels = width * height / inSampleSize; 253 | 254 | // Anything more than 2x the requested pixels we'll sample down further 255 | final long totalReqPixelsCap = reqWidth * reqHeight * 2; 256 | 257 | while (totalPixels > totalReqPixelsCap) { 258 | inSampleSize *= 2; 259 | totalPixels /= 2; 260 | } 261 | } 262 | return inSampleSize; 263 | // END_INCLUDE (calculate_sample_size) 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /bubblecloud/src/main/java/com/dodola/bubblecloud/bitmapfun/ImageWorker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 The Android Open Source Project 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.dodola.bubblecloud.bitmapfun; 18 | 19 | import android.content.Context; 20 | import android.content.res.Resources; 21 | import android.graphics.Bitmap; 22 | import android.graphics.BitmapFactory; 23 | import android.graphics.drawable.BitmapDrawable; 24 | import android.graphics.drawable.ColorDrawable; 25 | import android.graphics.drawable.Drawable; 26 | import android.graphics.drawable.TransitionDrawable; 27 | import android.support.v4.app.FragmentActivity; 28 | import android.support.v4.app.FragmentManager; 29 | import android.util.Log; 30 | import android.widget.ImageView; 31 | 32 | import com.dodola.bubblecloud.BuildConfig; 33 | 34 | import java.lang.ref.WeakReference; 35 | 36 | /** 37 | * This class wraps up completing some arbitrary long running work when loading a bitmap to an 38 | * ImageView. It handles things like using a memory and disk cache, running the work in a background 39 | * thread and setting a placeholder image. 40 | */ 41 | public abstract class ImageWorker { 42 | private static final String TAG = "ImageWorker"; 43 | private static final int FADE_IN_TIME = 200; 44 | 45 | private ImageCache mImageCache; 46 | private ImageCache.ImageCacheParams mImageCacheParams; 47 | private Bitmap mLoadingBitmap; 48 | private boolean mFadeInBitmap = true; 49 | private boolean mExitTasksEarly = false; 50 | protected boolean mPauseWork = false; 51 | private final Object mPauseWorkLock = new Object(); 52 | 53 | protected Resources mResources; 54 | 55 | private static final int MESSAGE_CLEAR = 0; 56 | private static final int MESSAGE_INIT_DISK_CACHE = 1; 57 | private static final int MESSAGE_FLUSH = 2; 58 | private static final int MESSAGE_CLOSE = 3; 59 | 60 | protected ImageWorker(Context context) { 61 | mResources = context.getResources(); 62 | } 63 | 64 | /** 65 | * Load an image specified by the data parameter into an ImageView (override 66 | * {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and 67 | * disk cache will be used if an {@link ImageCache} has been added using 68 | * {@link ImageWorker#addImageCache(FragmentManager, ImageCache.ImageCacheParams)}. If the 69 | * image is found in the memory cache, it is set immediately, otherwise an {@link AsyncTask} 70 | * will be created to asynchronously load the bitmap. 71 | * 72 | * @param data The URL of the image to download. 73 | * @param imageView The ImageView to bind the downloaded image to. 74 | */ 75 | public void loadImage(Object data, ImageView imageView) { 76 | if (data == null) { 77 | return; 78 | } 79 | 80 | BitmapDrawable value = null; 81 | 82 | if (mImageCache != null) { 83 | value = mImageCache.getBitmapFromMemCache(String.valueOf(data)); 84 | } 85 | 86 | if (value != null) { 87 | // Bitmap found in memory cache 88 | imageView.setImageDrawable(value); 89 | } else if (cancelPotentialWork(data, imageView)) { 90 | //BEGIN_INCLUDE(execute_background_task) 91 | final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView); 92 | final AsyncDrawable asyncDrawable = 93 | new AsyncDrawable(mResources, mLoadingBitmap, task); 94 | imageView.setImageDrawable(asyncDrawable); 95 | 96 | // NOTE: This uses a custom version of AsyncTask that has been pulled from the 97 | // framework and slightly modified. Refer to the docs at the top of the class 98 | // for more info on what was changed. 99 | task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR); 100 | //END_INCLUDE(execute_background_task) 101 | } 102 | } 103 | 104 | /** 105 | * Set placeholder bitmap that shows when the the background thread is running. 106 | * 107 | * @param bitmap 108 | */ 109 | public void setLoadingImage(Bitmap bitmap) { 110 | mLoadingBitmap = bitmap; 111 | } 112 | 113 | /** 114 | * Set placeholder bitmap that shows when the the background thread is running. 115 | * 116 | * @param resId 117 | */ 118 | public void setLoadingImage(int resId) { 119 | mLoadingBitmap = BitmapFactory.decodeResource(mResources, resId); 120 | } 121 | 122 | /** 123 | * Adds an {@link ImageCache} to this {@link ImageWorker} to handle disk and memory bitmap 124 | * caching. 125 | * 126 | * @param fragmentManager 127 | * @param cacheParams The cache parameters to use for the image cache. 128 | */ 129 | public void addImageCache(FragmentManager fragmentManager, 130 | ImageCache.ImageCacheParams cacheParams) { 131 | mImageCacheParams = cacheParams; 132 | mImageCache = ImageCache.getInstance(fragmentManager, mImageCacheParams); 133 | new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE); 134 | } 135 | 136 | /** 137 | * Adds an {@link ImageCache} to this {@link ImageWorker} to handle disk and memory bitmap 138 | * caching. 139 | * 140 | * @param activity 141 | * @param diskCacheDirectoryName See 142 | * {@link ImageCache.ImageCacheParams#ImageCacheParams(Context, String)}. 143 | */ 144 | public void addImageCache(FragmentActivity activity, String diskCacheDirectoryName) { 145 | mImageCacheParams = new ImageCache.ImageCacheParams(activity, diskCacheDirectoryName); 146 | mImageCache = ImageCache.getInstance(activity.getSupportFragmentManager(), mImageCacheParams); 147 | new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE); 148 | } 149 | 150 | /** 151 | * If set to true, the image will fade-in once it has been loaded by the background thread. 152 | */ 153 | public void setImageFadeIn(boolean fadeIn) { 154 | mFadeInBitmap = fadeIn; 155 | } 156 | 157 | public void setExitTasksEarly(boolean exitTasksEarly) { 158 | mExitTasksEarly = exitTasksEarly; 159 | setPauseWork(false); 160 | } 161 | 162 | /** 163 | * Subclasses should override this to define any processing or work that must happen to produce 164 | * the final bitmap. This will be executed in a background thread and be long running. For 165 | * example, you could resize a large bitmap here, or pull down an image from the network. 166 | * 167 | * @param data The data to identify which image to process, as provided by 168 | * {@link ImageWorker#loadImage(Object, ImageView)} 169 | * @return The processed bitmap 170 | */ 171 | protected abstract Bitmap processBitmap(Object data); 172 | 173 | /** 174 | * @return The {@link ImageCache} object currently being used by this ImageWorker. 175 | */ 176 | protected ImageCache getImageCache() { 177 | return mImageCache; 178 | } 179 | 180 | /** 181 | * Cancels any pending work attached to the provided ImageView. 182 | * 183 | * @param imageView 184 | */ 185 | public static void cancelWork(ImageView imageView) { 186 | final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); 187 | if (bitmapWorkerTask != null) { 188 | bitmapWorkerTask.cancel(true); 189 | if (BuildConfig.DEBUG) { 190 | final Object bitmapData = bitmapWorkerTask.mData; 191 | Log.d(TAG, "cancelWork - cancelled work for " + bitmapData); 192 | } 193 | } 194 | } 195 | 196 | /** 197 | * Returns true if the current work has been canceled or if there was no work in 198 | * progress on this image view. 199 | * Returns false if the work in progress deals with the same data. The work is not 200 | * stopped in that case. 201 | */ 202 | public static boolean cancelPotentialWork(Object data, ImageView imageView) { 203 | //BEGIN_INCLUDE(cancel_potential_work) 204 | final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); 205 | 206 | if (bitmapWorkerTask != null) { 207 | final Object bitmapData = bitmapWorkerTask.mData; 208 | if (bitmapData == null || !bitmapData.equals(data)) { 209 | bitmapWorkerTask.cancel(true); 210 | if (BuildConfig.DEBUG) { 211 | Log.d(TAG, "cancelPotentialWork - cancelled work for " + data); 212 | } 213 | } else { 214 | // The same work is already in progress. 215 | return false; 216 | } 217 | } 218 | return true; 219 | //END_INCLUDE(cancel_potential_work) 220 | } 221 | 222 | /** 223 | * @param imageView Any imageView 224 | * @return Retrieve the currently active work task (if any) associated with this imageView. 225 | * null if there is no such task. 226 | */ 227 | private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { 228 | if (imageView != null) { 229 | final Drawable drawable = imageView.getDrawable(); 230 | if (drawable instanceof AsyncDrawable) { 231 | final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; 232 | return asyncDrawable.getBitmapWorkerTask(); 233 | } 234 | } 235 | return null; 236 | } 237 | 238 | /** 239 | * The actual AsyncTask that will asynchronously process the image. 240 | */ 241 | private class BitmapWorkerTask extends AsyncTask { 242 | private Object mData; 243 | private final WeakReference imageViewReference; 244 | 245 | public BitmapWorkerTask(Object data, ImageView imageView) { 246 | mData = data; 247 | imageViewReference = new WeakReference(imageView); 248 | } 249 | 250 | /** 251 | * Background processing. 252 | */ 253 | @Override 254 | protected BitmapDrawable doInBackground(Void... params) { 255 | //BEGIN_INCLUDE(load_bitmap_in_background) 256 | if (BuildConfig.DEBUG) { 257 | Log.d(TAG, "doInBackground - starting work"); 258 | } 259 | 260 | final String dataString = String.valueOf(mData); 261 | Bitmap bitmap = null; 262 | BitmapDrawable drawable = null; 263 | 264 | // Wait here if work is paused and the task is not cancelled 265 | synchronized (mPauseWorkLock) { 266 | while (mPauseWork && !isCancelled()) { 267 | try { 268 | mPauseWorkLock.wait(); 269 | } catch (InterruptedException e) { 270 | } 271 | } 272 | } 273 | 274 | // If the image cache is available and this task has not been cancelled by another 275 | // thread and the ImageView that was originally bound to this task is still bound back 276 | // to this task and our "exit early" flag is not set then try and fetch the bitmap from 277 | // the cache 278 | if (mImageCache != null && !isCancelled() && getAttachedImageView() != null 279 | && !mExitTasksEarly) { 280 | bitmap = mImageCache.getBitmapFromDiskCache(dataString); 281 | } 282 | 283 | // If the bitmap was not found in the cache and this task has not been cancelled by 284 | // another thread and the ImageView that was originally bound to this task is still 285 | // bound back to this task and our "exit early" flag is not set, then call the main 286 | // process method (as implemented by a subclass) 287 | if (bitmap == null && !isCancelled() && getAttachedImageView() != null 288 | && !mExitTasksEarly) { 289 | bitmap = processBitmap(mData); 290 | } 291 | 292 | // If the bitmap was processed and the image cache is available, then add the processed 293 | // bitmap to the cache for future use. Note we don't check if the task was cancelled 294 | // here, if it was, and the thread is still running, we may as well add the processed 295 | // bitmap to our cache as it might be used again in the future 296 | if (bitmap != null) { 297 | if (Utils.hasHoneycomb()) { 298 | // Running on Honeycomb or newer, so wrap in a standard BitmapDrawable 299 | drawable = new BitmapDrawable(mResources, bitmap); 300 | } else { 301 | // Running on Gingerbread or older, so wrap in a RecyclingBitmapDrawable 302 | // which will recycle automagically 303 | drawable = new RecyclingBitmapDrawable(mResources, bitmap); 304 | } 305 | 306 | if (mImageCache != null) { 307 | mImageCache.addBitmapToCache(dataString, drawable); 308 | } 309 | } 310 | 311 | if (BuildConfig.DEBUG) { 312 | Log.d(TAG, "doInBackground - finished work"); 313 | } 314 | 315 | return drawable; 316 | //END_INCLUDE(load_bitmap_in_background) 317 | } 318 | 319 | /** 320 | * Once the image is processed, associates it to the imageView 321 | */ 322 | @Override 323 | protected void onPostExecute(BitmapDrawable value) { 324 | //BEGIN_INCLUDE(complete_background_work) 325 | // if cancel was called on this task or the "exit early" flag is set then we're done 326 | if (isCancelled() || mExitTasksEarly) { 327 | value = null; 328 | } 329 | 330 | final ImageView imageView = getAttachedImageView(); 331 | if (value != null && imageView != null) { 332 | if (BuildConfig.DEBUG) { 333 | Log.d(TAG, "onPostExecute - setting bitmap"); 334 | } 335 | setImageDrawable(imageView, value); 336 | } 337 | //END_INCLUDE(complete_background_work) 338 | } 339 | 340 | @Override 341 | protected void onCancelled(BitmapDrawable value) { 342 | super.onCancelled(value); 343 | synchronized (mPauseWorkLock) { 344 | mPauseWorkLock.notifyAll(); 345 | } 346 | } 347 | 348 | /** 349 | * Returns the ImageView associated with this task as long as the ImageView's task still 350 | * points to this task as well. Returns null otherwise. 351 | */ 352 | private ImageView getAttachedImageView() { 353 | final ImageView imageView = imageViewReference.get(); 354 | final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); 355 | 356 | if (this == bitmapWorkerTask) { 357 | return imageView; 358 | } 359 | 360 | return null; 361 | } 362 | } 363 | 364 | /** 365 | * A custom Drawable that will be attached to the imageView while the work is in progress. 366 | * Contains a reference to the actual worker task, so that it can be stopped if a new binding is 367 | * required, and makes sure that only the last started worker process can bind its result, 368 | * independently of the finish order. 369 | */ 370 | private static class AsyncDrawable extends BitmapDrawable { 371 | private final WeakReference bitmapWorkerTaskReference; 372 | 373 | public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { 374 | super(res, bitmap); 375 | bitmapWorkerTaskReference = 376 | new WeakReference(bitmapWorkerTask); 377 | } 378 | 379 | public BitmapWorkerTask getBitmapWorkerTask() { 380 | return bitmapWorkerTaskReference.get(); 381 | } 382 | } 383 | 384 | /** 385 | * Called when the processing is complete and the final drawable should be 386 | * set on the ImageView. 387 | * 388 | * @param imageView 389 | * @param drawable 390 | */ 391 | private void setImageDrawable(ImageView imageView, Drawable drawable) { 392 | if (mFadeInBitmap) { 393 | // Transition drawable with a transparent drawable and the final drawable 394 | final TransitionDrawable td = 395 | new TransitionDrawable(new Drawable[]{ 396 | new ColorDrawable(android.R.color.transparent), 397 | drawable 398 | }); 399 | // Set background to loading bitmap 400 | imageView.setBackgroundDrawable( 401 | new BitmapDrawable(mResources, mLoadingBitmap)); 402 | 403 | imageView.setImageDrawable(td); 404 | td.startTransition(FADE_IN_TIME); 405 | } else { 406 | imageView.setImageDrawable(drawable); 407 | } 408 | } 409 | 410 | /** 411 | * Pause any ongoing background work. This can be used as a temporary 412 | * measure to improve performance. For example background work could 413 | * be paused when a ListView or GridView is being scrolled using a 414 | * {@link android.widget.AbsListView.OnScrollListener} to keep 415 | * scrolling smooth. 416 | *

417 | * If work is paused, be sure setPauseWork(false) is called again 418 | * before your fragment or activity is destroyed (for example during 419 | * {@link android.app.Activity#onPause()}), or there is a risk the 420 | * background thread will never finish. 421 | */ 422 | public void setPauseWork(boolean pauseWork) { 423 | synchronized (mPauseWorkLock) { 424 | mPauseWork = pauseWork; 425 | if (!mPauseWork) { 426 | mPauseWorkLock.notifyAll(); 427 | } 428 | } 429 | } 430 | 431 | protected class CacheAsyncTask extends AsyncTask { 432 | 433 | @Override 434 | protected Void doInBackground(Object... params) { 435 | switch ((Integer) params[0]) { 436 | case MESSAGE_CLEAR: 437 | clearCacheInternal(); 438 | break; 439 | case MESSAGE_INIT_DISK_CACHE: 440 | initDiskCacheInternal(); 441 | break; 442 | case MESSAGE_FLUSH: 443 | flushCacheInternal(); 444 | break; 445 | case MESSAGE_CLOSE: 446 | closeCacheInternal(); 447 | break; 448 | } 449 | return null; 450 | } 451 | } 452 | 453 | protected void initDiskCacheInternal() { 454 | if (mImageCache != null) { 455 | mImageCache.initDiskCache(); 456 | } 457 | } 458 | 459 | protected void clearCacheInternal() { 460 | if (mImageCache != null) { 461 | mImageCache.clearCache(); 462 | } 463 | } 464 | 465 | protected void flushCacheInternal() { 466 | if (mImageCache != null) { 467 | mImageCache.flush(); 468 | } 469 | } 470 | 471 | protected void closeCacheInternal() { 472 | if (mImageCache != null) { 473 | mImageCache.close(); 474 | mImageCache = null; 475 | } 476 | } 477 | 478 | public void clearCache() { 479 | new CacheAsyncTask().execute(MESSAGE_CLEAR); 480 | } 481 | 482 | public void flushCache() { 483 | new CacheAsyncTask().execute(MESSAGE_FLUSH); 484 | } 485 | 486 | public void closeCache() { 487 | new CacheAsyncTask().execute(MESSAGE_CLOSE); 488 | } 489 | } 490 | -------------------------------------------------------------------------------- /bubblecloud/src/main/java/com/dodola/bubblecloud/bitmapfun/RecyclingBitmapDrawable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 The Android Open Source Project 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.dodola.bubblecloud.bitmapfun; 18 | 19 | import android.content.res.Resources; 20 | import android.graphics.Bitmap; 21 | import android.graphics.drawable.BitmapDrawable; 22 | 23 | 24 | /** 25 | * A BitmapDrawable that keeps track of whether it is being displayed or cached. 26 | * When the drawable is no longer being displayed or cached, 27 | * {@link Bitmap#recycle() recycle()} will be called on this drawable's bitmap. 28 | */ 29 | public class RecyclingBitmapDrawable extends BitmapDrawable { 30 | 31 | static final String TAG = "CountingBitmapDrawable"; 32 | 33 | private int mCacheRefCount = 0; 34 | private int mDisplayRefCount = 0; 35 | 36 | private boolean mHasBeenDisplayed; 37 | 38 | public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) { 39 | super(res, bitmap); 40 | } 41 | 42 | /** 43 | * Notify the drawable that the displayed state has changed. Internally a 44 | * count is kept so that the drawable knows when it is no longer being 45 | * displayed. 46 | * 47 | * @param isDisplayed - Whether the drawable is being displayed or not 48 | */ 49 | public void setIsDisplayed(boolean isDisplayed) { 50 | //BEGIN_INCLUDE(set_is_displayed) 51 | synchronized (this) { 52 | if (isDisplayed) { 53 | mDisplayRefCount++; 54 | mHasBeenDisplayed = true; 55 | } else { 56 | mDisplayRefCount--; 57 | } 58 | } 59 | 60 | // Check to see if recycle() can be called 61 | checkState(); 62 | //END_INCLUDE(set_is_displayed) 63 | } 64 | 65 | /** 66 | * Notify the drawable that the cache state has changed. Internally a count 67 | * is kept so that the drawable knows when it is no longer being cached. 68 | * 69 | * @param isCached - Whether the drawable is being cached or not 70 | */ 71 | public void setIsCached(boolean isCached) { 72 | //BEGIN_INCLUDE(set_is_cached) 73 | synchronized (this) { 74 | if (isCached) { 75 | mCacheRefCount++; 76 | } else { 77 | mCacheRefCount--; 78 | } 79 | } 80 | 81 | // Check to see if recycle() can be called 82 | checkState(); 83 | //END_INCLUDE(set_is_cached) 84 | } 85 | 86 | private synchronized void checkState() { 87 | //BEGIN_INCLUDE(check_state) 88 | // If the drawable cache and display ref counts = 0, and this drawable 89 | // has been displayed, then recycle 90 | if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed 91 | && hasValidBitmap()) { 92 | 93 | getBitmap().recycle(); 94 | } 95 | //END_INCLUDE(check_state) 96 | } 97 | 98 | private synchronized boolean hasValidBitmap() { 99 | Bitmap bitmap = getBitmap(); 100 | return bitmap != null && !bitmap.isRecycled(); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /bubblecloud/src/main/java/com/dodola/bubblecloud/bitmapfun/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 The Android Open Source Project 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.dodola.bubblecloud.bitmapfun; 18 | 19 | import android.annotation.TargetApi; 20 | import android.os.Build; 21 | import android.os.Build.VERSION_CODES; 22 | import android.os.StrictMode; 23 | 24 | 25 | /** 26 | * Class containing some static utility methods. 27 | */ 28 | public class Utils { 29 | private Utils() { 30 | } 31 | 32 | 33 | public static boolean hasFroyo() { 34 | // Can use static final constants like FROYO, declared in later versions 35 | // of the OS since they are inlined at compile time. This is guaranteed behavior. 36 | return Build.VERSION.SDK_INT >= VERSION_CODES.FROYO; 37 | } 38 | 39 | public static boolean hasGingerbread() { 40 | return Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD; 41 | } 42 | 43 | public static boolean hasHoneycomb() { 44 | return Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; 45 | } 46 | 47 | public static boolean hasHoneycombMR1() { 48 | return Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1; 49 | } 50 | 51 | public static boolean hasJellyBean() { 52 | return Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; 53 | } 54 | 55 | public static boolean hasKitKat() { 56 | return Build.VERSION.SDK_INT >= VERSION_CODES.KITKAT; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /bubblecloud/src/main/java/com/dodola/bubblecloud/utils/FileManagerImageLoader.java: -------------------------------------------------------------------------------- 1 | package com.dodola.bubblecloud.utils; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.IOException; 6 | import java.lang.ref.SoftReference; 7 | import java.lang.ref.WeakReference; 8 | import java.lang.reflect.Field; 9 | import java.util.concurrent.Executor; 10 | import java.util.concurrent.Executors; 11 | import java.util.concurrent.ThreadPoolExecutor; 12 | 13 | import android.annotation.TargetApi; 14 | import android.app.Application; 15 | import android.content.Context; 16 | import android.content.res.Resources; 17 | import android.database.Cursor; 18 | import android.graphics.Bitmap; 19 | import android.graphics.BitmapFactory; 20 | import android.graphics.drawable.BitmapDrawable; 21 | import android.graphics.drawable.Drawable; 22 | import android.media.ThumbnailUtils; 23 | import android.net.Uri; 24 | import android.os.Build; 25 | import android.os.Environment; 26 | import android.provider.MediaStore.Files.FileColumns; 27 | import android.provider.MediaStore.Images; 28 | import android.provider.MediaStore.Video; 29 | import android.text.TextUtils; 30 | import android.util.SparseArray; 31 | import android.view.View; 32 | import android.widget.ImageView; 33 | 34 | import com.dodola.bubblecloud.bitmapfun.AsyncTask; 35 | import com.dodola.bubblecloud.bitmapfun.ImageCache; 36 | import com.dodola.bubblecloud.bitmapfun.ImageFetcher; 37 | import com.dodola.bubblecloud.bitmapfun.ImageResizer; 38 | import com.dodola.bubblecloud.bitmapfun.RecyclingBitmapDrawable; 39 | 40 | public class FileManagerImageLoader { 41 | 42 | private SparseArray> defaultBitmap = new SparseArray>(); 43 | public static final Executor DUAL_THREAD_EXECUTOR = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); 44 | public boolean isExitApp = true; // 是否退出app 45 | private static final int MINI_KIND = 1; 46 | // private static final int FULL_SCREEN_KIND = 2; 47 | // private static final int MICRO_KIND = 3; 48 | private final Object mPauseWorkLock = new Object(); 49 | protected boolean mPauseWork = false; 50 | private boolean mExitTasksEarly = false; 51 | protected Resources mResources; 52 | private Context mContext; 53 | protected static ImageCache mImageCache; 54 | private static FileManagerImageLoader instance; 55 | 56 | public synchronized static FileManagerImageLoader getInstance() { 57 | return instance; 58 | } 59 | 60 | public static synchronized void prepare(Application appContext) { 61 | if (instance == null) { 62 | instance = new FileManagerImageLoader(appContext); 63 | ImageCache.ImageCacheParams cacheParams = 64 | new ImageCache.ImageCacheParams(appContext, Environment.getExternalStorageDirectory().getAbsolutePath()); 65 | cacheParams.setMemCacheSizePercent(0.25f); 66 | mImageCache = ImageCache.getInstance(cacheParams); 67 | } 68 | } 69 | 70 | private FileManagerImageLoader(Context context) { 71 | mResources = context.getResources(); 72 | mContext = context; 73 | } 74 | 75 | /** 76 | * 启动下载调度程序 77 | */ 78 | public void startDownLoadThread() { 79 | isExitApp = false; 80 | } 81 | 82 | /** 83 | * 停止所有任务调�? 84 | */ 85 | public void endDownLoadThread() { 86 | try { 87 | // mImageCache.clearMemoryCache(); 88 | ThreadPoolExecutor ex = (ThreadPoolExecutor) DUAL_THREAD_EXECUTOR; 89 | ex.getQueue().clear(); 90 | } catch (Exception ex) { 91 | 92 | } 93 | } 94 | 95 | public void setExitTasksEarly(boolean exitTasksEarly) { 96 | mExitTasksEarly = exitTasksEarly; 97 | setPauseWork(false); 98 | } 99 | 100 | public void setPauseWork(boolean pauseWork) { 101 | synchronized (mPauseWorkLock) { 102 | mPauseWork = pauseWork; 103 | if (!mPauseWork) { 104 | mPauseWorkLock.notifyAll(); 105 | } 106 | } 107 | } 108 | 109 | private class BitmapWorkerTask extends AsyncTask { 110 | private Object data; 111 | private boolean isBig; 112 | private int mWidth, mHeight; 113 | private final WeakReference imageViewReference; 114 | public final WeakReference frameViewReference; 115 | 116 | public BitmapWorkerTask(ImageView imageView) { 117 | imageViewReference = new WeakReference(imageView); 118 | frameViewReference = null; 119 | } 120 | 121 | public BitmapWorkerTask(ImageView imageView, View frameView) { 122 | imageViewReference = new WeakReference(imageView); 123 | frameViewReference = new WeakReference(frameView); 124 | } 125 | 126 | @Override 127 | protected void onPreExecute() { 128 | super.onPreExecute(); 129 | 130 | } 131 | 132 | /** 133 | * Background processing. 134 | */ 135 | @Override 136 | protected BitmapDrawable doInBackground(Object... params) { 137 | 138 | synchronized (mPauseWorkLock) { 139 | while (mPauseWork && !isCancelled()) { 140 | try { 141 | mPauseWorkLock.wait(); 142 | } catch (InterruptedException e) { 143 | } 144 | } 145 | } 146 | String info = (String) params[0]; 147 | 148 | RecyclingBitmapDrawable drawable = null; 149 | 150 | if (info != null) { 151 | 152 | if (!isCancelled() && getAttachedImageView() != null && !mExitTasksEarly) { 153 | try { 154 | drawable = getIconDrawable(info); 155 | } catch (Throwable ex) { 156 | drawable = null; 157 | } 158 | } 159 | 160 | } 161 | 162 | if (drawable != null && !mPauseWork) { 163 | mImageCache.addBitmapToCache(info, drawable); 164 | } 165 | 166 | return drawable; 167 | } 168 | 169 | @Override 170 | protected void onPostExecute(BitmapDrawable value) { 171 | if (isCancelled() || mExitTasksEarly) { 172 | value = null; 173 | } 174 | 175 | final ImageView imageView = getAttachedImageView(); 176 | final View frameView = getAttachedFrameView(); 177 | if (value != null && imageView != null) { 178 | setImageDrawable(imageView, value); 179 | if (frameView != null) { 180 | frameView.setVisibility(View.GONE); 181 | } 182 | } 183 | } 184 | 185 | @Override 186 | protected void onCancelled(BitmapDrawable value) { 187 | super.onCancelled(value); 188 | synchronized (mPauseWorkLock) { 189 | mPauseWorkLock.notifyAll(); 190 | } 191 | } 192 | 193 | private ImageView getAttachedImageView() { 194 | final ImageView imageView = imageViewReference.get(); 195 | final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); 196 | 197 | if (this == bitmapWorkerTask) { 198 | return imageView; 199 | } 200 | 201 | return null; 202 | } 203 | 204 | private View getAttachedFrameView() { 205 | if (frameViewReference != null) { 206 | final View view = frameViewReference.get(); 207 | final ImageView imageView = imageViewReference.get(); 208 | final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); 209 | 210 | if (this == bitmapWorkerTask) { 211 | return view; 212 | } 213 | } 214 | return null; 215 | } 216 | } 217 | 218 | private RecyclingBitmapDrawable getIconDrawable(String info) { 219 | 220 | Drawable apkIcon = Utils.getApkIcon(mContext, info); 221 | if (BitmapDrawable.class.isInstance(apkIcon)) { 222 | BitmapDrawable icon = (BitmapDrawable) apkIcon; 223 | if (icon != null) 224 | return new RecyclingBitmapDrawable(mResources, icon.getBitmap()); 225 | else 226 | return null; 227 | } else { 228 | return null; 229 | } 230 | } 231 | 232 | 233 | private static class AsyncDrawable extends BitmapDrawable { 234 | private final WeakReference bitmapWorkerTaskReference; 235 | 236 | public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { 237 | super(res, bitmap); 238 | bitmapWorkerTaskReference = new WeakReference(bitmapWorkerTask); 239 | } 240 | 241 | public BitmapWorkerTask getBitmapWorkerTask() { 242 | return bitmapWorkerTaskReference.get(); 243 | } 244 | } 245 | 246 | private BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { 247 | BitmapWorkerTask task = null; 248 | if (imageView != null) { 249 | final Drawable drawable = imageView.getDrawable(); 250 | if (drawable instanceof AsyncDrawable) { 251 | final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; 252 | task = asyncDrawable.getBitmapWorkerTask(); 253 | } 254 | } 255 | return task; 256 | } 257 | 258 | private void setImageDrawable(ImageView imageView, BitmapDrawable drawable) { 259 | imageView.setImageDrawable(drawable); 260 | } 261 | 262 | public boolean cancelPotentialWork(Object data, ImageView imageView) { 263 | final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); 264 | if (bitmapWorkerTask != null) { 265 | final Object bitmapData = bitmapWorkerTask.data; 266 | if (bitmapData == null || !bitmapData.equals(data)) { 267 | bitmapWorkerTask.cancel(true); 268 | } else { 269 | return false; 270 | } 271 | } 272 | return true; 273 | } 274 | 275 | 276 | /** 277 | * 添加任务 278 | */ 279 | public void addTask(String info, ImageView imgIcon, Bitmap defaultBitmap, int width, int height, boolean isBig) { 280 | 281 | if (TextUtils.isEmpty(info) || imgIcon == null) 282 | return; 283 | 284 | BitmapDrawable bitmap = null; 285 | if (mImageCache != null && !isBig) { 286 | bitmap = mImageCache.getBitmapFromMemCache(info); 287 | } 288 | if (bitmap != null && bitmap.getBitmap() != null && !bitmap.getBitmap().isRecycled()) { 289 | imgIcon.setImageDrawable(bitmap); 290 | } else if (cancelPotentialWork(info, imgIcon)) { 291 | final BitmapWorkerTask workerTask = new BitmapWorkerTask(imgIcon); 292 | workerTask.isBig = isBig; 293 | workerTask.data = info; 294 | workerTask.mWidth = width; 295 | workerTask.mHeight = height; 296 | final AsyncDrawable asyncDrawable = new AsyncDrawable(mResources, defaultBitmap, workerTask); 297 | imgIcon.setImageDrawable(asyncDrawable); 298 | workerTask.executeOnExecutor(DUAL_THREAD_EXECUTOR, info); 299 | } 300 | } 301 | 302 | } 303 | -------------------------------------------------------------------------------- /bubblecloud/src/main/java/com/dodola/bubblecloud/utils/Utils.java: -------------------------------------------------------------------------------- 1 | package com.dodola.bubblecloud.utils; 2 | 3 | import android.content.Context; 4 | import android.content.pm.ApplicationInfo; 5 | import android.content.pm.PackageInfo; 6 | import android.content.pm.PackageManager; 7 | import android.graphics.drawable.Drawable; 8 | 9 | /** 10 | * Created by dodola on 15/7/24. 11 | */ 12 | public class Utils { 13 | /* 14 | * 采用了新的办法获取APK图标,之前的失败是因为android中存在的一个BUG,通过 appInfo.publicSourceDir = 15 | * apkPath;来修正这个问题,详情参见: 16 | * http://code.google.com/p/android/issues/detail?id=9151 17 | */ 18 | public static Drawable getApkIcon(Context context, String apkPath) { 19 | PackageManager pm = context.getPackageManager(); 20 | PackageInfo info = pm.getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES); 21 | if (info != null) { 22 | ApplicationInfo appInfo = info.applicationInfo; 23 | appInfo.sourceDir = apkPath; 24 | appInfo.publicSourceDir = apkPath; 25 | try { 26 | return appInfo.loadIcon(pm); 27 | } catch (OutOfMemoryError e) { 28 | } 29 | } 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /bubblecloud/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /bubblecloud/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 200dp 4 | 250dp 5 | 224dp 6 | 264dp 7 | 100dp 8 | 43dp 9 | 43dp 10 | 12dp 11 | -------------------------------------------------------------------------------- /bubblecloud/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | BubbleCloud 3 | 4 | -------------------------------------------------------------------------------- /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:1.2.3' 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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/BubbleCloudView/5c388b2ff8e40f6113091c71c34c5fcb654bd70f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jul 23 16:34:23 CST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip 7 | -------------------------------------------------------------------------------- /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 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /screenshot/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodola/BubbleCloudView/5c388b2ff8e40f6113091c71c34c5fcb654bd70f/screenshot/demo.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':bubblecloud' 2 | --------------------------------------------------------------------------------