diff --git a/samples/training/bitmapfun/AndroidManifest.xml b/samples/training/bitmapfun/AndroidManifest.xml new file mode 100644 index 000000000..4a6f0f541 --- /dev/null +++ b/samples/training/bitmapfun/AndroidManifest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/training/bitmapfun/libs/android-support-v4.jar b/samples/training/bitmapfun/libs/android-support-v4.jar new file mode 100644 index 000000000..99e063b33 Binary files /dev/null and b/samples/training/bitmapfun/libs/android-support-v4.jar differ diff --git a/samples/training/bitmapfun/project.properties b/samples/training/bitmapfun/project.properties new file mode 100644 index 000000000..0840b4a05 --- /dev/null +++ b/samples/training/bitmapfun/project.properties @@ -0,0 +1,14 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-15 diff --git a/samples/training/bitmapfun/res/drawable-hdpi/ic_launcher.png b/samples/training/bitmapfun/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 000000000..96a442e5b Binary files /dev/null and b/samples/training/bitmapfun/res/drawable-hdpi/ic_launcher.png differ diff --git a/samples/training/bitmapfun/res/drawable-ldpi/ic_launcher.png b/samples/training/bitmapfun/res/drawable-ldpi/ic_launcher.png new file mode 100644 index 000000000..99238729d Binary files /dev/null and b/samples/training/bitmapfun/res/drawable-ldpi/ic_launcher.png differ diff --git a/samples/training/bitmapfun/res/drawable-mdpi/ic_launcher.png b/samples/training/bitmapfun/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 000000000..359047dfa Binary files /dev/null and b/samples/training/bitmapfun/res/drawable-mdpi/ic_launcher.png differ diff --git a/samples/training/bitmapfun/res/drawable-nodpi/empty_photo.png b/samples/training/bitmapfun/res/drawable-nodpi/empty_photo.png new file mode 100644 index 000000000..da1478a51 Binary files /dev/null and b/samples/training/bitmapfun/res/drawable-nodpi/empty_photo.png differ diff --git a/samples/training/bitmapfun/res/drawable-xhdpi/ic_launcher.png b/samples/training/bitmapfun/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 000000000..71c6d760f Binary files /dev/null and b/samples/training/bitmapfun/res/drawable-xhdpi/ic_launcher.png differ diff --git a/samples/training/bitmapfun/res/layout/image_detail_fragment.xml b/samples/training/bitmapfun/res/layout/image_detail_fragment.xml new file mode 100644 index 000000000..2d9dfcbf8 --- /dev/null +++ b/samples/training/bitmapfun/res/layout/image_detail_fragment.xml @@ -0,0 +1,36 @@ + + + + + + + + + + \ No newline at end of file diff --git a/samples/training/bitmapfun/res/layout/image_detail_pager.xml b/samples/training/bitmapfun/res/layout/image_detail_pager.xml new file mode 100644 index 000000000..877a26bb5 --- /dev/null +++ b/samples/training/bitmapfun/res/layout/image_detail_pager.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/samples/training/bitmapfun/res/layout/image_grid_fragment.xml b/samples/training/bitmapfun/res/layout/image_grid_fragment.xml new file mode 100644 index 000000000..e2034dea1 --- /dev/null +++ b/samples/training/bitmapfun/res/layout/image_grid_fragment.xml @@ -0,0 +1,29 @@ + + + + + + \ No newline at end of file diff --git a/samples/training/bitmapfun/res/menu/main_menu.xml b/samples/training/bitmapfun/res/menu/main_menu.xml new file mode 100644 index 000000000..0e727d086 --- /dev/null +++ b/samples/training/bitmapfun/res/menu/main_menu.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/samples/training/bitmapfun/res/values-large/dimens.xml b/samples/training/bitmapfun/res/values-large/dimens.xml new file mode 100644 index 000000000..503f267b0 --- /dev/null +++ b/samples/training/bitmapfun/res/values-large/dimens.xml @@ -0,0 +1,23 @@ + + + + + + 150dp + 1dp + + \ No newline at end of file diff --git a/samples/training/bitmapfun/res/values-v11/styles.xml b/samples/training/bitmapfun/res/values-v11/styles.xml new file mode 100644 index 000000000..0c6452641 --- /dev/null +++ b/samples/training/bitmapfun/res/values-v11/styles.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/training/bitmapfun/res/values-xlarge/dimens.xml b/samples/training/bitmapfun/res/values-xlarge/dimens.xml new file mode 100644 index 000000000..0f4397751 --- /dev/null +++ b/samples/training/bitmapfun/res/values-xlarge/dimens.xml @@ -0,0 +1,23 @@ + + + + + + 198dp + 2dp + + \ No newline at end of file diff --git a/samples/training/bitmapfun/res/values/dimens.xml b/samples/training/bitmapfun/res/values/dimens.xml new file mode 100644 index 000000000..60d540f02 --- /dev/null +++ b/samples/training/bitmapfun/res/values/dimens.xml @@ -0,0 +1,24 @@ + + + + + + 100dp + 1dp + 80dp + + \ No newline at end of file diff --git a/samples/training/bitmapfun/res/values/strings.xml b/samples/training/bitmapfun/res/values/strings.xml new file mode 100644 index 000000000..b77f768a4 --- /dev/null +++ b/samples/training/bitmapfun/res/values/strings.xml @@ -0,0 +1,30 @@ + + + + + + BitmapFun + This is a sample application for the Android Training class + "Displaying Bitmaps Efficiently" + (http://developer.android.com/training/displaying-bitmaps/display-bitmap.html). It is not + designed to be a full reference application but to demonstrate the concepts discussed in + training course. + Clear Caches + Caches have been cleared + Image Thumbnail + + \ No newline at end of file diff --git a/samples/training/bitmapfun/res/values/styles.xml b/samples/training/bitmapfun/res/values/styles.xml new file mode 100644 index 000000000..3e72fd335 --- /dev/null +++ b/samples/training/bitmapfun/res/values/styles.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/provider/Images.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/provider/Images.java new file mode 100644 index 000000000..5c9ef5c61 --- /dev/null +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/provider/Images.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bitmapfun.provider; + +import com.example.android.bitmapfun.util.ImageWorker.ImageWorkerAdapter; + +/** + * Some simple test data to use for this sample app. + */ +public class Images { + + /** + * This are PicasaWeb URLs and could potentially change. Ideally the PicasaWeb API should be + * used to fetch the URLs. + */ + public final static String[] imageUrls = new String[] { + "https://lh6.googleusercontent.com/-jZgveEqb6pg/T3R4kXScycI/AAAAAAAAAE0/xQ7CvpfXDzc/s1024/sample_image_01.jpg", + "https://lh4.googleusercontent.com/-K2FMuOozxU0/T3R4lRAiBTI/AAAAAAAAAE8/a3Eh9JvnnzI/s1024/sample_image_02.jpg", + "https://lh5.googleusercontent.com/-SCS5C646rxM/T3R4l7QB6xI/AAAAAAAAAFE/xLcuVv3CUyA/s1024/sample_image_03.jpg", + "https://lh6.googleusercontent.com/-f0NJR6-_Thg/T3R4mNex2wI/AAAAAAAAAFI/45oug4VE8MI/s1024/sample_image_04.jpg", + "https://lh3.googleusercontent.com/-n-xcJmiI0pg/T3R4mkSchHI/AAAAAAAAAFU/EoiNNb7kk3A/s1024/sample_image_05.jpg", + "https://lh3.googleusercontent.com/-X43vAudm7f4/T3R4nGSChJI/AAAAAAAAAFk/3bna6D-2EE8/s1024/sample_image_06.jpg", + "https://lh5.googleusercontent.com/-MpZneqIyjXU/T3R4nuGO1aI/AAAAAAAAAFg/r09OPjLx1ZY/s1024/sample_image_07.jpg", + "https://lh6.googleusercontent.com/-ql3YNfdClJo/T3XvW9apmFI/AAAAAAAAAL4/_6HFDzbahc4/s1024/sample_image_08.jpg", + "https://lh5.googleusercontent.com/-Pxa7eqF4cyc/T3R4oasvPEI/AAAAAAAAAF0/-uYDH92h8LA/s1024/sample_image_09.jpg", + "https://lh4.googleusercontent.com/-Li-rjhFEuaI/T3R4o-VUl4I/AAAAAAAAAF8/5E5XdMnP1oE/s1024/sample_image_10.jpg", + "https://lh5.googleusercontent.com/-_HU4fImgFhA/T3R4pPVIwWI/AAAAAAAAAGA/0RfK_Vkgth4/s1024/sample_image_11.jpg", + "https://lh6.googleusercontent.com/-0gnNrVjwa0Y/T3R4peGYJwI/AAAAAAAAAGU/uX_9wvRPM9I/s1024/sample_image_12.jpg", + "https://lh3.googleusercontent.com/-HBxuzALS_Zs/T3R4qERykaI/AAAAAAAAAGQ/_qQ16FaZ1q0/s1024/sample_image_13.jpg", + "https://lh4.googleusercontent.com/-cKojDrARNjQ/T3R4qfWSGPI/AAAAAAAAAGY/MR5dnbNaPyY/s1024/sample_image_14.jpg", + "https://lh3.googleusercontent.com/-WujkdYfcyZ8/T3R4qrIMGUI/AAAAAAAAAGk/277LIdgvnjg/s1024/sample_image_15.jpg", + "https://lh6.googleusercontent.com/-FMHR7Vy3PgI/T3R4rOXlEKI/AAAAAAAAAGs/VeXrDNDBkaw/s1024/sample_image_16.jpg", + "https://lh4.googleusercontent.com/-mrR0AJyNTH0/T3R4rZs6CuI/AAAAAAAAAG0/UE1wQqCOqLA/s1024/sample_image_17.jpg", + "https://lh6.googleusercontent.com/-z77w0eh3cow/T3R4rnLn05I/AAAAAAAAAG4/BaerfWoNucU/s1024/sample_image_18.jpg", + "https://lh5.googleusercontent.com/-aWVwh1OU5Bk/T3R4sAWw0yI/AAAAAAAAAHE/4_KAvJttFwA/s1024/sample_image_19.jpg", + "https://lh6.googleusercontent.com/-q-js52DMnWQ/T3R4tZhY2sI/AAAAAAAAAHM/A8kjp2Ivdqg/s1024/sample_image_20.jpg", + "https://lh5.googleusercontent.com/-_jIzvvzXKn4/T3R4t7xpdVI/AAAAAAAAAHU/7QC6eZ10jgs/s1024/sample_image_21.jpg", + "https://lh3.googleusercontent.com/-lnGi4IMLpwU/T3R4uCMa7vI/AAAAAAAAAHc/1zgzzz6qTpk/s1024/sample_image_22.jpg", + "https://lh5.googleusercontent.com/-fFCzKjFPsPc/T3R4u0SZPFI/AAAAAAAAAHk/sbgjzrktOK0/s1024/sample_image_23.jpg", + "https://lh4.googleusercontent.com/-8TqoW5gBE_Y/T3R4vBS3NPI/AAAAAAAAAHs/EZYvpNsaNXk/s1024/sample_image_24.jpg", + "https://lh6.googleusercontent.com/-gc4eQ3ySdzs/T3R4vafoA7I/AAAAAAAAAH4/yKii5P6tqDE/s1024/sample_image_25.jpg", + "https://lh5.googleusercontent.com/--NYOPCylU7Q/T3R4vjAiWkI/AAAAAAAAAH8/IPNx5q3ptRA/s1024/sample_image_26.jpg", + "https://lh6.googleusercontent.com/-9IJM8so4vCI/T3R4vwJO2yI/AAAAAAAAAIE/ljlr-cwuqZM/s1024/sample_image_27.jpg", + "https://lh4.googleusercontent.com/-KW6QwOHfhBs/T3R4w0RsQiI/AAAAAAAAAIM/uEFLVgHPFCk/s1024/sample_image_28.jpg", + "https://lh4.googleusercontent.com/-z2557Ec1ctY/T3R4x3QA2hI/AAAAAAAAAIk/9-GzPL1lTWE/s1024/sample_image_29.jpg", + "https://lh5.googleusercontent.com/-LaKXAn4Kr1c/T3R4yc5b4lI/AAAAAAAAAIY/fMgcOVQfmD0/s1024/sample_image_30.jpg", + "https://lh4.googleusercontent.com/-F9LRToJoQdo/T3R4yrLtyQI/AAAAAAAAAIg/ri9uUCWuRmo/s1024/sample_image_31.jpg", + "https://lh4.googleusercontent.com/-6X-xBwP-QpI/T3R4zGVboII/AAAAAAAAAIs/zYH4PjjngY0/s1024/sample_image_32.jpg", + "https://lh5.googleusercontent.com/-VdLRjbW4LAs/T3R4zXu3gUI/AAAAAAAAAIw/9aFp9t7mCPg/s1024/sample_image_33.jpg", + "https://lh6.googleusercontent.com/-gL6R17_fDJU/T3R4zpIXGjI/AAAAAAAAAI8/Q2Vjx-L9X20/s1024/sample_image_34.jpg", + "https://lh3.googleusercontent.com/-1fGH4YJXEzo/T3R40Y1B7KI/AAAAAAAAAJE/MnTsa77g-nk/s1024/sample_image_35.jpg", + "https://lh4.googleusercontent.com/-Ql0jHSrea-A/T3R403mUfFI/AAAAAAAAAJM/qzI4SkcH9tY/s1024/sample_image_36.jpg", + "https://lh5.googleusercontent.com/-BL5FIBR_tzI/T3R41DA0AKI/AAAAAAAAAJk/GZfeeb-SLM0/s1024/sample_image_37.jpg", + "https://lh4.googleusercontent.com/-wF2Vc9YDutw/T3R41fR2BCI/AAAAAAAAAJc/JdU1sHdMRAk/s1024/sample_image_38.jpg", + "https://lh6.googleusercontent.com/-ZWHiPehwjTI/T3R41zuaKCI/AAAAAAAAAJg/hR3QJ1v3REg/s1024/sample_image_39.jpg", + }; + + /** + * This are PicasaWeb thumbnail URLs and could potentially change. Ideally the PicasaWeb API + * should be used to fetch the URLs. + */ + public final static String[] imageThumbUrls = new String[] { + "https://lh6.googleusercontent.com/-jZgveEqb6pg/T3R4kXScycI/AAAAAAAAAE0/xQ7CvpfXDzc/s160-c/sample_image_01.jpg", + "https://lh4.googleusercontent.com/-K2FMuOozxU0/T3R4lRAiBTI/AAAAAAAAAE8/a3Eh9JvnnzI/s160-c/sample_image_02.jpg", + "https://lh5.googleusercontent.com/-SCS5C646rxM/T3R4l7QB6xI/AAAAAAAAAFE/xLcuVv3CUyA/s160-c/sample_image_03.jpg", + "https://lh6.googleusercontent.com/-f0NJR6-_Thg/T3R4mNex2wI/AAAAAAAAAFI/45oug4VE8MI/s160-c/sample_image_04.jpg", + "https://lh3.googleusercontent.com/-n-xcJmiI0pg/T3R4mkSchHI/AAAAAAAAAFU/EoiNNb7kk3A/s160-c/sample_image_05.jpg", + "https://lh3.googleusercontent.com/-X43vAudm7f4/T3R4nGSChJI/AAAAAAAAAFk/3bna6D-2EE8/s160-c/sample_image_06.jpg", + "https://lh5.googleusercontent.com/-MpZneqIyjXU/T3R4nuGO1aI/AAAAAAAAAFg/r09OPjLx1ZY/s160-c/sample_image_07.jpg", + "https://lh6.googleusercontent.com/-ql3YNfdClJo/T3XvW9apmFI/AAAAAAAAAL4/_6HFDzbahc4/s160-c/sample_image_08.jpg", + "https://lh5.googleusercontent.com/-Pxa7eqF4cyc/T3R4oasvPEI/AAAAAAAAAF0/-uYDH92h8LA/s160-c/sample_image_09.jpg", + "https://lh4.googleusercontent.com/-Li-rjhFEuaI/T3R4o-VUl4I/AAAAAAAAAF8/5E5XdMnP1oE/s160-c/sample_image_10.jpg", + "https://lh5.googleusercontent.com/-_HU4fImgFhA/T3R4pPVIwWI/AAAAAAAAAGA/0RfK_Vkgth4/s160-c/sample_image_11.jpg", + "https://lh6.googleusercontent.com/-0gnNrVjwa0Y/T3R4peGYJwI/AAAAAAAAAGU/uX_9wvRPM9I/s160-c/sample_image_12.jpg", + "https://lh3.googleusercontent.com/-HBxuzALS_Zs/T3R4qERykaI/AAAAAAAAAGQ/_qQ16FaZ1q0/s160-c/sample_image_13.jpg", + "https://lh4.googleusercontent.com/-cKojDrARNjQ/T3R4qfWSGPI/AAAAAAAAAGY/MR5dnbNaPyY/s160-c/sample_image_14.jpg", + "https://lh3.googleusercontent.com/-WujkdYfcyZ8/T3R4qrIMGUI/AAAAAAAAAGk/277LIdgvnjg/s160-c/sample_image_15.jpg", + "https://lh6.googleusercontent.com/-FMHR7Vy3PgI/T3R4rOXlEKI/AAAAAAAAAGs/VeXrDNDBkaw/s160-c/sample_image_16.jpg", + "https://lh4.googleusercontent.com/-mrR0AJyNTH0/T3R4rZs6CuI/AAAAAAAAAG0/UE1wQqCOqLA/s160-c/sample_image_17.jpg", + "https://lh6.googleusercontent.com/-z77w0eh3cow/T3R4rnLn05I/AAAAAAAAAG4/BaerfWoNucU/s160-c/sample_image_18.jpg", + "https://lh5.googleusercontent.com/-aWVwh1OU5Bk/T3R4sAWw0yI/AAAAAAAAAHE/4_KAvJttFwA/s160-c/sample_image_19.jpg", + "https://lh6.googleusercontent.com/-q-js52DMnWQ/T3R4tZhY2sI/AAAAAAAAAHM/A8kjp2Ivdqg/s160-c/sample_image_20.jpg", + "https://lh5.googleusercontent.com/-_jIzvvzXKn4/T3R4t7xpdVI/AAAAAAAAAHU/7QC6eZ10jgs/s160-c/sample_image_21.jpg", + "https://lh3.googleusercontent.com/-lnGi4IMLpwU/T3R4uCMa7vI/AAAAAAAAAHc/1zgzzz6qTpk/s160-c/sample_image_22.jpg", + "https://lh5.googleusercontent.com/-fFCzKjFPsPc/T3R4u0SZPFI/AAAAAAAAAHk/sbgjzrktOK0/s160-c/sample_image_23.jpg", + "https://lh4.googleusercontent.com/-8TqoW5gBE_Y/T3R4vBS3NPI/AAAAAAAAAHs/EZYvpNsaNXk/s160-c/sample_image_24.jpg", + "https://lh6.googleusercontent.com/-gc4eQ3ySdzs/T3R4vafoA7I/AAAAAAAAAH4/yKii5P6tqDE/s160-c/sample_image_25.jpg", + "https://lh5.googleusercontent.com/--NYOPCylU7Q/T3R4vjAiWkI/AAAAAAAAAH8/IPNx5q3ptRA/s160-c/sample_image_26.jpg", + "https://lh6.googleusercontent.com/-9IJM8so4vCI/T3R4vwJO2yI/AAAAAAAAAIE/ljlr-cwuqZM/s160-c/sample_image_27.jpg", + "https://lh4.googleusercontent.com/-KW6QwOHfhBs/T3R4w0RsQiI/AAAAAAAAAIM/uEFLVgHPFCk/s160-c/sample_image_28.jpg", + "https://lh4.googleusercontent.com/-z2557Ec1ctY/T3R4x3QA2hI/AAAAAAAAAIk/9-GzPL1lTWE/s160-c/sample_image_29.jpg", + "https://lh5.googleusercontent.com/-LaKXAn4Kr1c/T3R4yc5b4lI/AAAAAAAAAIY/fMgcOVQfmD0/s160-c/sample_image_30.jpg", + "https://lh4.googleusercontent.com/-F9LRToJoQdo/T3R4yrLtyQI/AAAAAAAAAIg/ri9uUCWuRmo/s160-c/sample_image_31.jpg", + "https://lh4.googleusercontent.com/-6X-xBwP-QpI/T3R4zGVboII/AAAAAAAAAIs/zYH4PjjngY0/s160-c/sample_image_32.jpg", + "https://lh5.googleusercontent.com/-VdLRjbW4LAs/T3R4zXu3gUI/AAAAAAAAAIw/9aFp9t7mCPg/s160-c/sample_image_33.jpg", + "https://lh6.googleusercontent.com/-gL6R17_fDJU/T3R4zpIXGjI/AAAAAAAAAI8/Q2Vjx-L9X20/s160-c/sample_image_34.jpg", + "https://lh3.googleusercontent.com/-1fGH4YJXEzo/T3R40Y1B7KI/AAAAAAAAAJE/MnTsa77g-nk/s160-c/sample_image_35.jpg", + "https://lh4.googleusercontent.com/-Ql0jHSrea-A/T3R403mUfFI/AAAAAAAAAJM/qzI4SkcH9tY/s160-c/sample_image_36.jpg", + "https://lh5.googleusercontent.com/-BL5FIBR_tzI/T3R41DA0AKI/AAAAAAAAAJk/GZfeeb-SLM0/s160-c/sample_image_37.jpg", + "https://lh4.googleusercontent.com/-wF2Vc9YDutw/T3R41fR2BCI/AAAAAAAAAJc/JdU1sHdMRAk/s160-c/sample_image_38.jpg", + "https://lh6.googleusercontent.com/-ZWHiPehwjTI/T3R41zuaKCI/AAAAAAAAAJg/hR3QJ1v3REg/s160-c/sample_image_39.jpg", + }; + + /** + * Simple static adapter to use for images. + */ + public final static ImageWorkerAdapter imageWorkerUrlsAdapter = new ImageWorkerAdapter() { + @Override + public Object getItem(int num) { + return Images.imageUrls[num]; + } + + @Override + public int getSize() { + return Images.imageUrls.length; + } + }; + + /** + * Simple static adapter to use for image thumbnails. + */ + public final static ImageWorkerAdapter imageThumbWorkerUrlsAdapter = new ImageWorkerAdapter() { + @Override + public Object getItem(int num) { + return Images.imageThumbUrls[num]; + } + + @Override + public int getSize() { + return Images.imageThumbUrls.length; + } + }; +} diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailActivity.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailActivity.java new file mode 100644 index 000000000..c7ee8cd4a --- /dev/null +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailActivity.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bitmapfun.ui; + +import android.annotation.SuppressLint; +import android.app.ActionBar; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.DisplayMetrics; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.WindowManager.LayoutParams; +import android.widget.Toast; + +import com.example.android.bitmapfun.R; +import com.example.android.bitmapfun.provider.Images; +import com.example.android.bitmapfun.util.DiskLruCache; +import com.example.android.bitmapfun.util.ImageCache; +import com.example.android.bitmapfun.util.ImageFetcher; +import com.example.android.bitmapfun.util.ImageResizer; +import com.example.android.bitmapfun.util.ImageWorker; +import com.example.android.bitmapfun.util.Utils; + +public class ImageDetailActivity extends FragmentActivity implements OnClickListener { + private static final String IMAGE_CACHE_DIR = "images"; + public static final String EXTRA_IMAGE = "extra_image"; + + private ImagePagerAdapter mAdapter; + private ImageResizer mImageWorker; + private ViewPager mPager; + + @SuppressLint("NewApi") + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.image_detail_pager); + + // Fetch screen height and width, to use as our max size when loading images as this + // activity runs full screen + final DisplayMetrics displaymetrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(displaymetrics); + final int height = displaymetrics.heightPixels; + final int width = displaymetrics.widthPixels; + final int longest = height > width ? height : width; + + // The ImageWorker takes care of loading images into our ImageView children asynchronously + mImageWorker = new ImageFetcher(this, longest); + mImageWorker.setAdapter(Images.imageWorkerUrlsAdapter); + mImageWorker.setImageCache(ImageCache.findOrCreateCache(this, IMAGE_CACHE_DIR)); + mImageWorker.setImageFadeIn(false); + + // Set up ViewPager and backing adapter + mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), + mImageWorker.getAdapter().getSize()); + mPager = (ViewPager) findViewById(R.id.pager); + mPager.setAdapter(mAdapter); + mPager.setPageMargin((int) getResources().getDimension(R.dimen.image_detail_pager_margin)); + + // Set up activity to go full screen + getWindow().addFlags(LayoutParams.FLAG_FULLSCREEN); + + // Enable some additional newer visibility and ActionBar features to create a more immersive + // photo viewing experience + if (Utils.hasActionBar()) { + final ActionBar actionBar = getActionBar(); + + // Enable "up" navigation on ActionBar icon and hide title text + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowTitleEnabled(false); + + // Start low profile mode and hide ActionBar + mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); + actionBar.hide(); + + // Hide and show the ActionBar as the visibility changes + mPager.setOnSystemUiVisibilityChangeListener( + new View.OnSystemUiVisibilityChangeListener() { + @Override + public void onSystemUiVisibilityChange(int vis) { + if ((vis & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0) { + actionBar.hide(); + } else { + actionBar.show(); + } + } + }); + } + + // Set the current item based on the extra passed in to this activity + final int extraCurrentItem = getIntent().getIntExtra(EXTRA_IMAGE, -1); + if (extraCurrentItem != -1) { + mPager.setCurrentItem(extraCurrentItem); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + // Home or "up" navigation + final Intent intent = new Intent(this, ImageGridActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + case R.id.clear_cache: + final ImageCache cache = mImageWorker.getImageCache(); + if (cache != null) { + mImageWorker.getImageCache().clearCaches(); + DiskLruCache.clearCache(this, ImageFetcher.HTTP_CACHE_DIR); + Toast.makeText(this, R.string.clear_cache_complete, + Toast.LENGTH_SHORT).show(); + } + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.main_menu, menu); + return true; + } + + /** + * Called by the ViewPager child fragments to load images via the one ImageWorker + * + * @return + */ + public ImageWorker getImageWorker() { + return mImageWorker; + } + + /** + * The main adapter that backs the ViewPager. A subclass of FragmentStatePagerAdapter as there + * could be a large number of items in the ViewPager and we don't want to retain them all in + * memory at once but create/destroy them on the fly. + */ + private class ImagePagerAdapter extends FragmentStatePagerAdapter { + private final int mSize; + + public ImagePagerAdapter(FragmentManager fm, int size) { + super(fm); + mSize = size; + } + + @Override + public int getCount() { + return mSize; + } + + @Override + public Fragment getItem(int position) { + return ImageDetailFragment.newInstance(position); + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + final ImageDetailFragment fragment = (ImageDetailFragment) object; + // As the item gets destroyed we try and cancel any existing work. + fragment.cancelWork(); + super.destroyItem(container, position, object); + } + } + + /** + * Set on the ImageView in the ViewPager children fragments, to enable/disable low profile mode + * when the ImageView is touched. + */ + @SuppressLint("NewApi") + @Override + public void onClick(View v) { + final int vis = mPager.getSystemUiVisibility(); + if ((vis & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0) { + mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); + } else { + mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); + } + } +} diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailFragment.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailFragment.java new file mode 100644 index 000000000..e2fd70385 --- /dev/null +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageDetailFragment.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bitmapfun.ui; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.example.android.bitmapfun.R; +import com.example.android.bitmapfun.util.ImageWorker; +import com.example.android.bitmapfun.util.Utils; + +/** + * This fragment will populate the children of the ViewPager from {@link ImageDetailActivity}. + */ +public class ImageDetailFragment extends Fragment { + private static final String IMAGE_DATA_EXTRA = "resId"; + private int mImageNum; + private ImageView mImageView; + private ImageWorker mImageWorker; + + /** + * Factory method to generate a new instance of the fragment given an image number. + * + * @param imageNum The image number within the parent adapter to load + * @return A new instance of ImageDetailFragment with imageNum extras + */ + public static ImageDetailFragment newInstance(int imageNum) { + final ImageDetailFragment f = new ImageDetailFragment(); + + final Bundle args = new Bundle(); + args.putInt(IMAGE_DATA_EXTRA, imageNum); + f.setArguments(args); + + return f; + } + + /** + * Empty constructor as per the Fragment documentation + */ + public ImageDetailFragment() {} + + /** + * Populate image number from extra, use the convenience factory method + * {@link ImageDetailFragment#newInstance(int)} to create this fragment. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate and locate the main ImageView + final View v = inflater.inflate(R.layout.image_detail_fragment, container, false); + mImageView = (ImageView) v.findViewById(R.id.imageView); + return v; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + // Use the parent activity to load the image asynchronously into the ImageView (so a single + // cache can be used over all pages in the ViewPager + if (ImageDetailActivity.class.isInstance(getActivity())) { + mImageWorker = ((ImageDetailActivity) getActivity()).getImageWorker(); + mImageWorker.loadImage(mImageNum, mImageView); + } + + // Pass clicks on the ImageView to the parent activity to handle + if (OnClickListener.class.isInstance(getActivity()) && Utils.hasActionBar()) { + mImageView.setOnClickListener((OnClickListener) getActivity()); + } + } + + /** + * Cancels the asynchronous work taking place on the ImageView, called by the adapter backing + * the ViewPager when the child is destroyed. + */ + public void cancelWork() { + ImageWorker.cancelWork(mImageView); + mImageView.setImageDrawable(null); + mImageView = null; + } +} diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridActivity.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridActivity.java new file mode 100644 index 000000000..28d97b345 --- /dev/null +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridActivity.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bitmapfun.ui; + +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentTransaction; + +/** + * Simple FragmentActivity to hold the main {@link ImageGridFragment} and not much else. + */ +public class ImageGridActivity extends FragmentActivity { + private static final String TAG = "ImageGridFragment"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getSupportFragmentManager().findFragmentByTag(TAG) == null) { + final FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); + ft.add(android.R.id.content, new ImageGridFragment(), TAG); + ft.commit(); + } + } +} diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridFragment.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridFragment.java new file mode 100644 index 000000000..495d405ba --- /dev/null +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/ui/ImageGridFragment.java @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bitmapfun.ui; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewTreeObserver; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.GridView; +import android.widget.ImageView; +import android.widget.Toast; + +import com.example.android.bitmapfun.BuildConfig; +import com.example.android.bitmapfun.R; +import com.example.android.bitmapfun.provider.Images; +import com.example.android.bitmapfun.util.DiskLruCache; +import com.example.android.bitmapfun.util.ImageCache; +import com.example.android.bitmapfun.util.ImageCache.ImageCacheParams; +import com.example.android.bitmapfun.util.ImageFetcher; +import com.example.android.bitmapfun.util.ImageResizer; +import com.example.android.bitmapfun.util.Utils; + +/** + * The main fragment that powers the ImageGridActivity screen. Fairly straight forward GridView + * implementation with the key addition being the ImageWorker class w/ImageCache to load children + * asynchronously, keeping the UI nice and smooth and caching thumbnails for quick retrieval. The + * cache is retained over configuration changes like orientation change so the images are populated + * quickly as the user rotates the device. + */ +public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener { + private static final String TAG = "ImageGridFragment"; + private static final String IMAGE_CACHE_DIR = "thumbs"; + + private int mImageThumbSize; + private int mImageThumbSpacing; + private ImageAdapter mAdapter; + private ImageResizer mImageWorker; + + /** + * Empty constructor as per the Fragment documentation + */ + public ImageGridFragment() {} + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + + mImageThumbSize = getResources().getDimensionPixelSize(R.dimen.image_thumbnail_size); + mImageThumbSpacing = getResources().getDimensionPixelSize(R.dimen.image_thumbnail_spacing); + + mAdapter = new ImageAdapter(getActivity()); + + ImageCacheParams cacheParams = new ImageCacheParams(IMAGE_CACHE_DIR); + + // Allocate a third of the per-app memory limit to the bitmap memory cache. This value + // should be chosen carefully based on a number of factors. Refer to the corresponding + // Android Training class for more discussion: + // http://developer.android.com/training/displaying-bitmaps/ + // In this case, we aren't using memory for much else other than this activity and the + // ImageDetailActivity so a third lets us keep all our sample image thumbnails in memory + // at once. + cacheParams.memCacheSize = 1024 * 1024 * Utils.getMemoryClass(getActivity()) / 3; + + // The ImageWorker takes care of loading images into our ImageView children asynchronously + mImageWorker = new ImageFetcher(getActivity(), mImageThumbSize); + mImageWorker.setAdapter(Images.imageThumbWorkerUrlsAdapter); + mImageWorker.setLoadingImage(R.drawable.empty_photo); + mImageWorker.setImageCache(ImageCache.findOrCreateCache(getActivity(), cacheParams)); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + + final View v = inflater.inflate(R.layout.image_grid_fragment, container, false); + final GridView mGridView = (GridView) v.findViewById(R.id.gridView); + mGridView.setAdapter(mAdapter); + mGridView.setOnItemClickListener(this); + + // This listener is used to get the final width of the GridView and then calculate the + // number of columns and the width of each column. The width of each column is variable + // as the GridView has stretchMode=columnWidth. The column width is used to set the height + // of each view so we get nice square thumbnails. + mGridView.getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (mAdapter.getNumColumns() == 0) { + final int numColumns = (int) Math.floor( + mGridView.getWidth() / (mImageThumbSize + mImageThumbSpacing)); + if (numColumns > 0) { + final int columnWidth = + (mGridView.getWidth() / numColumns) - mImageThumbSpacing; + mAdapter.setNumColumns(numColumns); + mAdapter.setItemHeight(columnWidth); + if (BuildConfig.DEBUG) { + Log.d(TAG, "onCreateView - numColumns set to " + numColumns); + } + } + } + } + }); + + return v; + } + + @Override + public void onResume() { + super.onResume(); + mImageWorker.setExitTasksEarly(false); + mAdapter.notifyDataSetChanged(); + } + + @Override + public void onPause() { + super.onPause(); + mImageWorker.setExitTasksEarly(true); + } + + @Override + public void onItemClick(AdapterView parent, View v, int position, long id) { + final Intent i = new Intent(getActivity(), ImageDetailActivity.class); + i.putExtra(ImageDetailActivity.EXTRA_IMAGE, (int) id); + startActivity(i); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.main_menu, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.clear_cache: + final ImageCache cache = mImageWorker.getImageCache(); + if (cache != null) { + mImageWorker.getImageCache().clearCaches(); + DiskLruCache.clearCache(getActivity(), ImageFetcher.HTTP_CACHE_DIR); + Toast.makeText(getActivity(), R.string.clear_cache_complete, + Toast.LENGTH_SHORT).show(); + } + return true; + } + return super.onOptionsItemSelected(item); + } + + /** + * The main adapter that backs the GridView. This is fairly standard except the number of + * columns in the GridView is used to create a fake top row of empty views as we use a + * transparent ActionBar and don't want the real top row of images to start off covered by it. + */ + private class ImageAdapter extends BaseAdapter { + + private final Context mContext; + private int mItemHeight = 0; + private int mNumColumns = 0; + private int mActionBarHeight = -1; + private GridView.LayoutParams mImageViewLayoutParams; + + public ImageAdapter(Context context) { + super(); + mContext = context; + mImageViewLayoutParams = new GridView.LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + } + + @Override + public int getCount() { + // Size of adapter + number of columns for top empty row + return mImageWorker.getAdapter().getSize() + mNumColumns; + } + + @Override + public Object getItem(int position) { + return position < mNumColumns ? + null : mImageWorker.getAdapter().getItem(position - mNumColumns); + } + + @Override + public long getItemId(int position) { + return position < mNumColumns ? 0 : position - mNumColumns; + } + + @Override + public int getViewTypeCount() { + // Two types of views, the normal ImageView and the top row of empty views + return 2; + } + + @Override + public int getItemViewType(int position) { + return (position < mNumColumns) ? 1 : 0; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public View getView(int position, View convertView, ViewGroup container) { + // First check if this is the top row + if (position < mNumColumns) { + if (convertView == null) { + convertView = new View(mContext); + } + // Calculate ActionBar height + if (mActionBarHeight < 0) { + TypedValue tv = new TypedValue(); + if (mContext.getTheme().resolveAttribute( + android.R.attr.actionBarSize, tv, true)) { + mActionBarHeight = TypedValue.complexToDimensionPixelSize( + tv.data, mContext.getResources().getDisplayMetrics()); + } else { + // No ActionBar style (pre-Honeycomb or ActionBar not in theme) + mActionBarHeight = 0; + } + } + // Set empty view with height of ActionBar + convertView.setLayoutParams(new AbsListView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, mActionBarHeight)); + return convertView; + } + + // Now handle the main ImageView thumbnails + ImageView imageView; + if (convertView == null) { // if it's not recycled, instantiate and initialize + imageView = new ImageView(mContext); + imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); + imageView.setLayoutParams(mImageViewLayoutParams); + } else { // Otherwise re-use the converted view + imageView = (ImageView) convertView; + } + + // Check the height matches our calculated column width + if (imageView.getLayoutParams().height != mItemHeight) { + imageView.setLayoutParams(mImageViewLayoutParams); + } + + // Finally load the image asynchronously into the ImageView, this also takes care of + // setting a placeholder image while the background thread runs + mImageWorker.loadImage(position - mNumColumns, imageView); + return imageView; + } + + /** + * Sets the item height. Useful for when we know the column width so the height can be set + * to match. + * + * @param height + */ + public void setItemHeight(int height) { + if (height == mItemHeight) { + return; + } + mItemHeight = height; + mImageViewLayoutParams = + new GridView.LayoutParams(LayoutParams.MATCH_PARENT, mItemHeight); + mImageWorker.setImageSize(height); + notifyDataSetChanged(); + } + + public void setNumColumns(int numColumns) { + mNumColumns = numColumns; + } + + public int getNumColumns() { + return mNumColumns; + } + } +} diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/DiskLruCache.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/DiskLruCache.java new file mode 100644 index 000000000..a9f2166ea --- /dev/null +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/DiskLruCache.java @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bitmapfun.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.BitmapFactory; +import android.os.Environment; +import android.util.Log; + +import com.example.android.bitmapfun.BuildConfig; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * A simple disk LRU bitmap cache to illustrate how a disk cache would be used for bitmap caching. A + * much more robust and efficient disk LRU cache solution can be found in the ICS source code + * (libcore/luni/src/main/java/libcore/io/DiskLruCache.java) and is preferable to this simple + * implementation. + */ +public class DiskLruCache { + private static final String TAG = "DiskLruCache"; + private static final String CACHE_FILENAME_PREFIX = "cache_"; + private static final int MAX_REMOVALS = 4; + private static final int INITIAL_CAPACITY = 32; + private static final float LOAD_FACTOR = 0.75f; + + private final File mCacheDir; + private int cacheSize = 0; + private int cacheByteSize = 0; + private final int maxCacheItemSize = 64; // 64 item default + private long maxCacheByteSize = 1024 * 1024 * 5; // 5MB default + private CompressFormat mCompressFormat = CompressFormat.JPEG; + private int mCompressQuality = 70; + + private final Map mLinkedHashMap = + Collections.synchronizedMap(new LinkedHashMap( + INITIAL_CAPACITY, LOAD_FACTOR, true)); + + /** + * A filename filter to use to identify the cache filenames which have CACHE_FILENAME_PREFIX + * prepended. + */ + private static final FilenameFilter cacheFileFilter = new FilenameFilter() { + @Override + public boolean accept(File dir, String filename) { + return filename.startsWith(CACHE_FILENAME_PREFIX); + } + }; + + /** + * Used to fetch an instance of DiskLruCache. + * + * @param context + * @param cacheDir + * @param maxByteSize + * @return + */ + public static DiskLruCache openCache(Context context, File cacheDir, long maxByteSize) { + if (!cacheDir.exists()) { + cacheDir.mkdir(); + } + + if (cacheDir.isDirectory() && cacheDir.canWrite() + && Utils.getUsableSpace(cacheDir) > maxByteSize) { + return new DiskLruCache(cacheDir, maxByteSize); + } + + return null; + } + + /** + * Constructor that should not be called directly, instead use + * {@link DiskLruCache#openCache(Context, File, long)} which runs some extra checks before + * creating a DiskLruCache instance. + * + * @param cacheDir + * @param maxByteSize + */ + private DiskLruCache(File cacheDir, long maxByteSize) { + mCacheDir = cacheDir; + maxCacheByteSize = maxByteSize; + } + + /** + * Add a bitmap to the disk cache. + * + * @param key A unique identifier for the bitmap. + * @param data The bitmap to store. + */ + public void put(String key, Bitmap data) { + synchronized (mLinkedHashMap) { + if (mLinkedHashMap.get(key) == null) { + try { + final String file = createFilePath(mCacheDir, key); + if (writeBitmapToFile(data, file)) { + put(key, file); + flushCache(); + } + } catch (final FileNotFoundException e) { + Log.e(TAG, "Error in put: " + e.getMessage()); + } catch (final IOException e) { + Log.e(TAG, "Error in put: " + e.getMessage()); + } + } + } + } + + private void put(String key, String file) { + mLinkedHashMap.put(key, file); + cacheSize = mLinkedHashMap.size(); + cacheByteSize += new File(file).length(); + } + + /** + * Flush the cache, removing oldest entries if the total size is over the specified cache size. + * Note that this isn't keeping track of stale files in the cache directory that aren't in the + * HashMap. If the images and keys in the disk cache change often then they probably won't ever + * be removed. + */ + private void flushCache() { + Entry eldestEntry; + File eldestFile; + long eldestFileSize; + int count = 0; + + while (count < MAX_REMOVALS && + (cacheSize > maxCacheItemSize || cacheByteSize > maxCacheByteSize)) { + eldestEntry = mLinkedHashMap.entrySet().iterator().next(); + eldestFile = new File(eldestEntry.getValue()); + eldestFileSize = eldestFile.length(); + mLinkedHashMap.remove(eldestEntry.getKey()); + eldestFile.delete(); + cacheSize = mLinkedHashMap.size(); + cacheByteSize -= eldestFileSize; + count++; + if (BuildConfig.DEBUG) { + Log.d(TAG, "flushCache - Removed cache file, " + eldestFile + ", " + + eldestFileSize); + } + } + } + + /** + * Get an image from the disk cache. + * + * @param key The unique identifier for the bitmap + * @return The bitmap or null if not found + */ + public Bitmap get(String key) { + synchronized (mLinkedHashMap) { + final String file = mLinkedHashMap.get(key); + if (file != null) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Disk cache hit"); + } + return BitmapFactory.decodeFile(file); + } else { + final String existingFile = createFilePath(mCacheDir, key); + if (new File(existingFile).exists()) { + put(key, existingFile); + if (BuildConfig.DEBUG) { + Log.d(TAG, "Disk cache hit (existing file)"); + } + return BitmapFactory.decodeFile(existingFile); + } + } + return null; + } + } + + /** + * Checks if a specific key exist in the cache. + * + * @param key The unique identifier for the bitmap + * @return true if found, false otherwise + */ + public boolean containsKey(String key) { + // See if the key is in our HashMap + if (mLinkedHashMap.containsKey(key)) { + return true; + } + + // Now check if there's an actual file that exists based on the key + final String existingFile = createFilePath(mCacheDir, key); + if (new File(existingFile).exists()) { + // File found, add it to the HashMap for future use + put(key, existingFile); + return true; + } + return false; + } + + /** + * Removes all disk cache entries from this instance cache dir + */ + public void clearCache() { + DiskLruCache.clearCache(mCacheDir); + } + + /** + * Removes all disk cache entries from the application cache directory in the uniqueName + * sub-directory. + * + * @param context The context to use + * @param uniqueName A unique cache directory name to append to the app cache directory + */ + public static void clearCache(Context context, String uniqueName) { + File cacheDir = getDiskCacheDir(context, uniqueName); + clearCache(cacheDir); + } + + /** + * Removes all disk cache entries from the given directory. This should not be called directly, + * call {@link DiskLruCache#clearCache(Context, String)} or {@link DiskLruCache#clearCache()} + * instead. + * + * @param cacheDir The directory to remove the cache files from + */ + private static void clearCache(File cacheDir) { + final File[] files = cacheDir.listFiles(cacheFileFilter); + for (int i=0; i mMemoryCache; + + /** + * Creating a new ImageCache object using the specified parameters. + * + * @param context The context to use + * @param cacheParams The cache parameters to use to initialize the cache + */ + public ImageCache(Context context, ImageCacheParams cacheParams) { + init(context, cacheParams); + } + + /** + * Creating a new ImageCache object using the default parameters. + * + * @param context The context to use + * @param uniqueName A unique name that will be appended to the cache directory + */ + public ImageCache(Context context, String uniqueName) { + init(context, new ImageCacheParams(uniqueName)); + } + + /** + * Find and return an existing ImageCache stored in a {@link RetainFragment}, if not found a new + * one is created with defaults and saved to a {@link RetainFragment}. + * + * @param activity The calling {@link FragmentActivity} + * @param uniqueName A unique name to append to the cache directory + * @return An existing retained ImageCache object or a new one if one did not exist. + */ + public static ImageCache findOrCreateCache( + final FragmentActivity activity, final String uniqueName) { + return findOrCreateCache(activity, new ImageCacheParams(uniqueName)); + } + + /** + * Find and return an existing ImageCache stored in a {@link RetainFragment}, if not found a new + * one is created using the supplied params and saved to a {@link RetainFragment}. + * + * @param activity The calling {@link FragmentActivity} + * @param cacheParams The cache parameters to use if creating the ImageCache + * @return An existing retained ImageCache object or a new one if one did not exist + */ + public static ImageCache findOrCreateCache( + final FragmentActivity activity, ImageCacheParams cacheParams) { + + // Search for, or create an instance of the non-UI RetainFragment + final RetainFragment mRetainFragment = RetainFragment.findOrCreateRetainFragment( + activity.getSupportFragmentManager()); + + // See if we already have an ImageCache stored in RetainFragment + ImageCache imageCache = (ImageCache) mRetainFragment.getObject(); + + // No existing ImageCache, create one and store it in RetainFragment + if (imageCache == null) { + imageCache = new ImageCache(activity, cacheParams); + mRetainFragment.setObject(imageCache); + } + + return imageCache; + } + + /** + * Initialize the cache, providing all parameters. + * + * @param context The context to use + * @param cacheParams The cache parameters to initialize the cache + */ + private void init(Context context, ImageCacheParams cacheParams) { + final File diskCacheDir = DiskLruCache.getDiskCacheDir(context, cacheParams.uniqueName); + + // Set up disk cache + if (cacheParams.diskCacheEnabled) { + mDiskCache = DiskLruCache.openCache(context, diskCacheDir, cacheParams.diskCacheSize); + mDiskCache.setCompressParams(cacheParams.compressFormat, cacheParams.compressQuality); + if (cacheParams.clearDiskCacheOnStart) { + mDiskCache.clearCache(); + } + } + + // Set up memory cache + if (cacheParams.memoryCacheEnabled) { + mMemoryCache = new LruCache(cacheParams.memCacheSize) { + /** + * Measure item size in bytes rather than units which is more practical for a bitmap + * cache + */ + @Override + protected int sizeOf(String key, Bitmap bitmap) { + return Utils.getBitmapSize(bitmap); + } + }; + } + } + + public void addBitmapToCache(String data, Bitmap bitmap) { + if (data == null || bitmap == null) { + return; + } + + // Add to memory cache + if (mMemoryCache != null && mMemoryCache.get(data) == null) { + mMemoryCache.put(data, bitmap); + } + + // Add to disk cache + if (mDiskCache != null && !mDiskCache.containsKey(data)) { + mDiskCache.put(data, bitmap); + } + } + + /** + * Get from memory cache. + * + * @param data Unique identifier for which item to get + * @return The bitmap if found in cache, null otherwise + */ + public Bitmap getBitmapFromMemCache(String data) { + if (mMemoryCache != null) { + final Bitmap memBitmap = mMemoryCache.get(data); + if (memBitmap != null) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Memory cache hit"); + } + return memBitmap; + } + } + return null; + } + + /** + * Get from disk cache. + * + * @param data Unique identifier for which item to get + * @return The bitmap if found in cache, null otherwise + */ + public Bitmap getBitmapFromDiskCache(String data) { + if (mDiskCache != null) { + return mDiskCache.get(data); + } + return null; + } + + public void clearCaches() { + mDiskCache.clearCache(); + mMemoryCache.evictAll(); + } + + /** + * A holder class that contains cache parameters. + */ + public static class ImageCacheParams { + public String uniqueName; + public int memCacheSize = DEFAULT_MEM_CACHE_SIZE; + public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE; + public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT; + public int compressQuality = DEFAULT_COMPRESS_QUALITY; + public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED; + public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED; + public boolean clearDiskCacheOnStart = DEFAULT_CLEAR_DISK_CACHE_ON_START; + + public ImageCacheParams(String uniqueName) { + this.uniqueName = uniqueName; + } + } +} diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageFetcher.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageFetcher.java new file mode 100644 index 000000000..8b19dc32b --- /dev/null +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageFetcher.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bitmapfun.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.util.Log; +import android.widget.Toast; + +import com.example.android.bitmapfun.BuildConfig; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * A simple subclass of {@link ImageResizer} that fetches and resizes images fetched from a URL. + */ +public class ImageFetcher extends ImageResizer { + private static final String TAG = "ImageFetcher"; + private static final int HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10MB + public static final String HTTP_CACHE_DIR = "http"; + + /** + * Initialize providing a target image width and height for the processing images. + * + * @param context + * @param imageWidth + * @param imageHeight + */ + public ImageFetcher(Context context, int imageWidth, int imageHeight) { + super(context, imageWidth, imageHeight); + init(context); + } + + /** + * Initialize providing a single target image size (used for both width and height); + * + * @param context + * @param imageSize + */ + public ImageFetcher(Context context, int imageSize) { + super(context, imageSize); + init(context); + } + + private void init(Context context) { + checkConnection(context); + } + + /** + * Simple network connection check. + * + * @param context + */ + private void checkConnection(Context context) { + final ConnectivityManager cm = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + final NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + if (networkInfo == null || !networkInfo.isConnectedOrConnecting()) { + Toast.makeText(context, "No network connection found.", Toast.LENGTH_LONG).show(); + Log.e(TAG, "checkConnection - no connection found"); + } + } + + /** + * The main process method, which will be called by the ImageWorker in the AsyncTask background + * thread. + * + * @param data The data to load the bitmap, in this case, a regular http URL + * @return The downloaded and resized bitmap + */ + private Bitmap processBitmap(String data) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "processBitmap - " + data); + } + + // Download a bitmap, write it to a file + final File f = downloadBitmap(mContext, data); + + if (f != null) { + // Return a sampled down version + return decodeSampledBitmapFromFile(f.toString(), mImageWidth, mImageHeight); + } + + return null; + } + + @Override + protected Bitmap processBitmap(Object data) { + return processBitmap(String.valueOf(data)); + } + + /** + * Download a bitmap from a URL, write it to a disk and return the File pointer. This + * implementation uses a simple disk cache. + * + * @param context The context to use + * @param urlString The URL to fetch + * @return A File pointing to the fetched bitmap + */ + public static File downloadBitmap(Context context, String urlString) { + final File cacheDir = DiskLruCache.getDiskCacheDir(context, HTTP_CACHE_DIR); + + final DiskLruCache cache = + DiskLruCache.openCache(context, cacheDir, HTTP_CACHE_SIZE); + + final File cacheFile = new File(cache.createFilePath(urlString)); + + if (cache.containsKey(urlString)) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "downloadBitmap - found in http cache - " + urlString); + } + return cacheFile; + } + + if (BuildConfig.DEBUG) { + Log.d(TAG, "downloadBitmap - downloading - " + urlString); + } + + Utils.disableConnectionReuseIfNecessary(); + HttpURLConnection urlConnection = null; + BufferedOutputStream out = null; + + try { + final URL url = new URL(urlString); + urlConnection = (HttpURLConnection) url.openConnection(); + final InputStream in = + new BufferedInputStream(urlConnection.getInputStream(), Utils.IO_BUFFER_SIZE); + out = new BufferedOutputStream(new FileOutputStream(cacheFile), Utils.IO_BUFFER_SIZE); + + int b; + while ((b = in.read()) != -1) { + out.write(b); + } + + return cacheFile; + + } catch (final IOException e) { + Log.e(TAG, "Error in downloadBitmap - " + e); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + if (out != null) { + try { + out.close(); + } catch (final IOException e) { + Log.e(TAG, "Error in downloadBitmap - " + e); + } + } + } + + return null; + } +} diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageResizer.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageResizer.java new file mode 100644 index 000000000..18d1f82ff --- /dev/null +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageResizer.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bitmapfun.util; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Log; + +import com.example.android.bitmapfun.BuildConfig; + +/** + * A simple subclass of {@link ImageWorker} that resizes images from resources given a target width + * and height. Useful for when the input images might be too large to simply load directly into + * memory. + */ +public class ImageResizer extends ImageWorker { + private static final String TAG = "ImageWorker"; + protected int mImageWidth; + protected int mImageHeight; + + /** + * Initialize providing a single target image size (used for both width and height); + * + * @param context + * @param imageWidth + * @param imageHeight + */ + public ImageResizer(Context context, int imageWidth, int imageHeight) { + super(context); + setImageSize(imageWidth, imageHeight); + } + + /** + * Initialize providing a single target image size (used for both width and height); + * + * @param context + * @param imageSize + */ + public ImageResizer(Context context, int imageSize) { + super(context); + setImageSize(imageSize); + } + + /** + * Set the target image width and height. + * + * @param width + * @param height + */ + public void setImageSize(int width, int height) { + mImageWidth = width; + mImageHeight = height; + } + + /** + * Set the target image size (width and height will be the same). + * + * @param size + */ + public void setImageSize(int size) { + setImageSize(size, size); + } + + /** + * The main processing method. This happens in a background task. In this case we are just + * sampling down the bitmap and returning it from a resource. + * + * @param resId + * @return + */ + private Bitmap processBitmap(int resId) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "processBitmap - " + resId); + } + return decodeSampledBitmapFromResource( + mContext.getResources(), resId, mImageWidth, mImageHeight); + } + + @Override + protected Bitmap processBitmap(Object data) { + return processBitmap(Integer.parseInt(String.valueOf(data))); + } + + /** + * Decode and sample down a bitmap from resources to the requested width and height. + * + * @param res The resources object containing the image data + * @param resId The resource id of the image data + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @return A bitmap sampled down from the original with the same aspect ratio and dimensions + * that are equal to or greater than the requested width and height + */ + public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, + int reqWidth, int reqHeight) { + + // First decode with inJustDecodeBounds=true to check dimensions + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeResource(res, resId, options); + + // Calculate inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; + return BitmapFactory.decodeResource(res, resId, options); + } + + /** + * Decode and sample down a bitmap from a file to the requested width and height. + * + * @param filename The full path of the file to decode + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @return A bitmap sampled down from the original with the same aspect ratio and dimensions + * that are equal to or greater than the requested width and height + */ + public static synchronized Bitmap decodeSampledBitmapFromFile(String filename, + int reqWidth, int reqHeight) { + + // First decode with inJustDecodeBounds=true to check dimensions + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(filename, options); + + // Calculate inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; + return BitmapFactory.decodeFile(filename, options); + } + + /** + * Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding + * bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates + * the closest inSampleSize that will result in the final decoded bitmap having a width and + * height equal to or larger than the requested width and height. This implementation does not + * ensure a power of 2 is returned for inSampleSize which can be faster when decoding but + * results in a larger bitmap which isn't as useful for caching purposes. + * + * @param options An options object with out* params already populated (run through a decode* + * method with inJustDecodeBounds==true + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @return The value to be used for inSampleSize + */ + public static int calculateInSampleSize(BitmapFactory.Options options, + int reqWidth, int reqHeight) { + // Raw height and width of image + final int height = options.outHeight; + final int width = options.outWidth; + int inSampleSize = 1; + + if (height > reqHeight || width > reqWidth) { + if (width > height) { + inSampleSize = Math.round((float) height / (float) reqHeight); + } else { + inSampleSize = Math.round((float) width / (float) reqWidth); + } + + // This offers some additional logic in case the image has a strange + // aspect ratio. For example, a panorama may have a much larger + // width than height. In these cases the total pixels might still + // end up being too large to fit comfortably in memory, so we should + // be more aggressive with sample down the image (=larger + // inSampleSize). + + final float totalPixels = width * height; + + // Anything more than 2x the requested pixels we'll sample down + // further. + final float totalReqPixelsCap = reqWidth * reqHeight * 2; + + while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) { + inSampleSize++; + } + } + return inSampleSize; + } +} diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageWorker.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageWorker.java new file mode 100644 index 000000000..a0d26930d --- /dev/null +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/ImageWorker.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bitmapfun.util; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.os.AsyncTask; +import android.util.Log; +import android.widget.ImageView; + +import com.example.android.bitmapfun.BuildConfig; + +import java.lang.ref.WeakReference; + +/** + * This class wraps up completing some arbitrary long running work when loading a bitmap to an + * ImageView. It handles things like using a memory and disk cache, running the work in a background + * thread and setting a placeholder image. + */ +public abstract class ImageWorker { + private static final String TAG = "ImageWorker"; + private static final int FADE_IN_TIME = 200; + + private ImageCache mImageCache; + private Bitmap mLoadingBitmap; + private boolean mFadeInBitmap = true; + private boolean mExitTasksEarly = false; + + protected Context mContext; + protected ImageWorkerAdapter mImageWorkerAdapter; + + protected ImageWorker(Context context) { + mContext = context; + } + + /** + * Load an image specified by the data parameter into an ImageView (override + * {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and disk + * cache will be used if an {@link ImageCache} has been set using + * {@link ImageWorker#setImageCache(ImageCache)}. If the image is found in the memory cache, it + * is set immediately, otherwise an {@link AsyncTask} will be created to asynchronously load the + * bitmap. + * + * @param data The URL of the image to download. + * @param imageView The ImageView to bind the downloaded image to. + */ + public void loadImage(Object data, ImageView imageView) { + Bitmap bitmap = null; + + if (mImageCache != null) { + bitmap = mImageCache.getBitmapFromMemCache(String.valueOf(data)); + } + + if (bitmap != null) { + // Bitmap found in memory cache + imageView.setImageBitmap(bitmap); + } else if (cancelPotentialWork(data, imageView)) { + final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final AsyncDrawable asyncDrawable = + new AsyncDrawable(mContext.getResources(), mLoadingBitmap, task); + imageView.setImageDrawable(asyncDrawable); + task.execute(data); + } + } + + /** + * Load an image specified from a set adapter into an ImageView (override + * {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and disk + * cache will be used if an {@link ImageCache} has been set using + * {@link ImageWorker#setImageCache(ImageCache)}. If the image is found in the memory cache, it + * is set immediately, otherwise an {@link AsyncTask} will be created to asynchronously load the + * bitmap. {@link ImageWorker#setAdapter(ImageWorkerAdapter)} must be called before using this + * method. + * + * @param data The URL of the image to download. + * @param imageView The ImageView to bind the downloaded image to. + */ + public void loadImage(int num, ImageView imageView) { + if (mImageWorkerAdapter != null) { + loadImage(mImageWorkerAdapter.getItem(num), imageView); + } else { + throw new NullPointerException("Data not set, must call setAdapter() first."); + } + } + + /** + * Set placeholder bitmap that shows when the the background thread is running. + * + * @param bitmap + */ + public void setLoadingImage(Bitmap bitmap) { + mLoadingBitmap = bitmap; + } + + /** + * Set placeholder bitmap that shows when the the background thread is running. + * + * @param resId + */ + public void setLoadingImage(int resId) { + mLoadingBitmap = BitmapFactory.decodeResource(mContext.getResources(), resId); + } + + /** + * Set the {@link ImageCache} object to use with this ImageWorker. + * + * @param cacheCallback + */ + public void setImageCache(ImageCache cacheCallback) { + mImageCache = cacheCallback; + } + + public ImageCache getImageCache() { + return mImageCache; + } + + /** + * If set to true, the image will fade-in once it has been loaded by the background thread. + * + * @param fadeIn + */ + public void setImageFadeIn(boolean fadeIn) { + mFadeInBitmap = fadeIn; + } + + public void setExitTasksEarly(boolean exitTasksEarly) { + mExitTasksEarly = exitTasksEarly; + } + + /** + * Subclasses should override this to define any processing or work that must happen to produce + * the final bitmap. This will be executed in a background thread and be long running. For + * example, you could resize a large bitmap here, or pull down an image from the network. + * + * @param data The data to identify which image to process, as provided by + * {@link ImageWorker#loadImage(Object, ImageView)} + * @return The processed bitmap + */ + protected abstract Bitmap processBitmap(Object data); + + public static void cancelWork(ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + if (bitmapWorkerTask != null) { + bitmapWorkerTask.cancel(true); + if (BuildConfig.DEBUG) { + final Object bitmapData = bitmapWorkerTask.data; + Log.d(TAG, "cancelWork - cancelled work for " + bitmapData); + } + } + } + + /** + * Returns true if the current work has been canceled or if there was no work in + * progress on this image view. + * Returns false if the work in progress deals with the same data. The work is not + * stopped in that case. + */ + public static boolean cancelPotentialWork(Object data, ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Object bitmapData = bitmapWorkerTask.data; + if (bitmapData == null || !bitmapData.equals(data)) { + bitmapWorkerTask.cancel(true); + if (BuildConfig.DEBUG) { + Log.d(TAG, "cancelPotentialWork - cancelled work for " + data); + } + } else { + // The same work is already in progress. + return false; + } + } + return true; + } + + /** + * @param imageView Any imageView + * @return Retrieve the currently active work task (if any) associated with this imageView. + * null if there is no such task. + */ + private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + + /** + * The actual AsyncTask that will asynchronously process the image. + */ + private class BitmapWorkerTask extends AsyncTask { + private Object data; + private final WeakReference imageViewReference; + + public BitmapWorkerTask(ImageView imageView) { + imageViewReference = new WeakReference(imageView); + } + + /** + * Background processing. + */ + @Override + protected Bitmap doInBackground(Object... params) { + data = params[0]; + final String dataString = String.valueOf(data); + Bitmap bitmap = null; + + // If the image cache is available and this task has not been cancelled by another + // thread and the ImageView that was originally bound to this task is still bound back + // to this task and our "exit early" flag is not set then try and fetch the bitmap from + // the cache + if (mImageCache != null && !isCancelled() && getAttachedImageView() != null + && !mExitTasksEarly) { + bitmap = mImageCache.getBitmapFromDiskCache(dataString); + } + + // If the bitmap was not found in the cache and this task has not been cancelled by + // another thread and the ImageView that was originally bound to this task is still + // bound back to this task and our "exit early" flag is not set, then call the main + // process method (as implemented by a subclass) + if (bitmap == null && !isCancelled() && getAttachedImageView() != null + && !mExitTasksEarly) { + bitmap = processBitmap(params[0]); + } + + // If the bitmap was processed and the image cache is available, then add the processed + // bitmap to the cache for future use. Note we don't check if the task was cancelled + // here, if it was, and the thread is still running, we may as well add the processed + // bitmap to our cache as it might be used again in the future + if (bitmap != null && mImageCache != null) { + mImageCache.addBitmapToCache(dataString, bitmap); + } + + return bitmap; + } + + /** + * Once the image is processed, associates it to the imageView + */ + @Override + protected void onPostExecute(Bitmap bitmap) { + // if cancel was called on this task or the "exit early" flag is set then we're done + if (isCancelled() || mExitTasksEarly) { + bitmap = null; + } + + final ImageView imageView = getAttachedImageView(); + if (bitmap != null && imageView != null) { + setImageBitmap(imageView, bitmap); + } + } + + /** + * Returns the ImageView associated with this task as long as the ImageView's task still + * points to this task as well. Returns null otherwise. + */ + private ImageView getAttachedImageView() { + final ImageView imageView = imageViewReference.get(); + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (this == bitmapWorkerTask) { + return imageView; + } + + return null; + } + } + + /** + * A custom Drawable that will be attached to the imageView while the work is in progress. + * Contains a reference to the actual worker task, so that it can be stopped if a new binding is + * required, and makes sure that only the last started worker process can bind its result, + * independently of the finish order. + */ + private static class AsyncDrawable extends BitmapDrawable { + private final WeakReference bitmapWorkerTaskReference; + + public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + + bitmapWorkerTaskReference = + new WeakReference(bitmapWorkerTask); + } + + public BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } + } + + /** + * Called when the processing is complete and the final bitmap should be set on the ImageView. + * + * @param imageView + * @param bitmap + */ + private void setImageBitmap(ImageView imageView, Bitmap bitmap) { + if (mFadeInBitmap) { + // Transition drawable with a transparent drwabale and the final bitmap + final TransitionDrawable td = + new TransitionDrawable(new Drawable[] { + new ColorDrawable(android.R.color.transparent), + new BitmapDrawable(mContext.getResources(), bitmap) + }); + // Set background to loading bitmap + imageView.setBackgroundDrawable( + new BitmapDrawable(mContext.getResources(), mLoadingBitmap)); + + imageView.setImageDrawable(td); + td.startTransition(FADE_IN_TIME); + } else { + imageView.setImageBitmap(bitmap); + } + } + + /** + * Set the simple adapter which holds the backing data. + * + * @param adapter + */ + public void setAdapter(ImageWorkerAdapter adapter) { + mImageWorkerAdapter = adapter; + } + + /** + * Get the current adapter. + * + * @return + */ + public ImageWorkerAdapter getAdapter() { + return mImageWorkerAdapter; + } + + /** + * A very simple adapter for use with ImageWorker class and subclasses. + */ + public static abstract class ImageWorkerAdapter { + public abstract Object getItem(int num); + public abstract int getSize(); + } +} diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/RetainFragment.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/RetainFragment.java new file mode 100644 index 000000000..3ee9cd63f --- /dev/null +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/RetainFragment.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bitmapfun.util; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; + +/** + * A simple non-UI Fragment that stores a single Object and is retained over configuration changes. + * In this sample it will be used to retain the ImageCache object. + */ +public class RetainFragment extends Fragment { + private static final String TAG = "RetainFragment"; + private Object mObject; + + /** + * Empty constructor as per the Fragment documentation + */ + public RetainFragment() {} + + /** + * Locate an existing instance of this Fragment or if not found, create and + * add it using FragmentManager. + * + * @param fm The FragmentManager manager to use. + * @return The existing instance of the Fragment or the new instance if just + * created. + */ + public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) { + // Check to see if we have retained the worker fragment. + RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG); + + // If not retained (or first time running), we need to create and add it. + if (mRetainFragment == null) { + mRetainFragment = new RetainFragment(); + fm.beginTransaction().add(mRetainFragment, TAG).commit(); + } + + return mRetainFragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Make sure this Fragment is retained over a configuration change + setRetainInstance(true); + } + + /** + * Store a single object in this Fragment. + * + * @param object The object to store + */ + public void setObject(Object object) { + mObject = object; + } + + /** + * Get the stored object. + * + * @return The stored object + */ + public Object getObject() { + return mObject; + } + +} diff --git a/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/Utils.java b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/Utils.java new file mode 100644 index 000000000..544df339a --- /dev/null +++ b/samples/training/bitmapfun/src/com/example/android/bitmapfun/util/Utils.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bitmapfun.util; + +import android.annotation.SuppressLint; +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Build; +import android.os.Environment; +import android.os.StatFs; + +import java.io.File; + +/** + * Class containing some static utility methods. + */ +public class Utils { + public static final int IO_BUFFER_SIZE = 8 * 1024; + + private Utils() {}; + + /** + * Workaround for bug pre-Froyo, see here for more info: + * http://android-developers.blogspot.com/2011/09/androids-http-clients.html + */ + public static void disableConnectionReuseIfNecessary() { + // HTTP connection reuse which was buggy pre-froyo + if (hasHttpConnectionBug()) { + System.setProperty("http.keepAlive", "false"); + } + } + + /** + * Get the size in bytes of a bitmap. + * @param bitmap + * @return size in bytes + */ + @SuppressLint("NewApi") + public static int getBitmapSize(Bitmap bitmap) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { + return bitmap.getByteCount(); + } + // Pre HC-MR1 + return bitmap.getRowBytes() * bitmap.getHeight(); + } + + /** + * Check if external storage is built-in or removable. + * + * @return True if external storage is removable (like an SD card), false + * otherwise. + */ + @SuppressLint("NewApi") + public static boolean isExternalStorageRemovable() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { + return Environment.isExternalStorageRemovable(); + } + return true; + } + + /** + * Get the external app cache directory. + * + * @param context The context to use + * @return The external cache dir + */ + @SuppressLint("NewApi") + public static File getExternalCacheDir(Context context) { + if (hasExternalCacheDir()) { + return context.getExternalCacheDir(); + } + + // Before Froyo we need to construct the external cache dir ourselves + final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/"; + return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir); + } + + /** + * Check how much usable space is available at a given path. + * + * @param path The path to check + * @return The space available in bytes + */ + @SuppressLint("NewApi") + public static long getUsableSpace(File path) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { + return path.getUsableSpace(); + } + final StatFs stats = new StatFs(path.getPath()); + return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks(); + } + + /** + * Get the memory class of this device (approx. per-app memory limit) + * + * @param context + * @return + */ + public static int getMemoryClass(Context context) { + return ((ActivityManager) context.getSystemService( + Context.ACTIVITY_SERVICE)).getMemoryClass(); + } + + /** + * Check if OS version has a http URLConnection bug. See here for more information: + * http://android-developers.blogspot.com/2011/09/androids-http-clients.html + * + * @return + */ + public static boolean hasHttpConnectionBug() { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO; + } + + /** + * Check if OS version has built-in external cache dir method. + * + * @return + */ + public static boolean hasExternalCacheDir() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO; + } + + /** + * Check if ActionBar is available. + * + * @return + */ + public static boolean hasActionBar() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; + } +}