Merge "Updating code sample for "displaying bitmaps efficiently" training class. Changes: -Use updated versions of ImageWorker & ImageCache from I/O 2012 app -Use copied DiskLruCache from system (rather than custom) -Use copied AsyncTask from system (to keep behavior consistent) -Ensure no strict mode violations or lint errors -Other misc bug fixes -Move single-use static methods in Utils to corresponding class" into jb-dev

This commit is contained in:
Adam Koch
2012-08-28 07:42:10 -07:00
committed by Android (Google) Code Review
23 changed files with 2695 additions and 868 deletions

View File

@@ -22,7 +22,7 @@
<uses-sdk <uses-sdk
android:minSdkVersion="7" android:minSdkVersion="7"
android:targetSdkVersion="15" /> android:targetSdkVersion="16" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
@@ -36,7 +36,10 @@
<activity <activity
android:name=".ui.ImageDetailActivity" android:name=".ui.ImageDetailActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:parentActivityName=".ui.ImageGridActivity"
android:theme="@style/AppTheme.FullScreen" > android:theme="@style/AppTheme.FullScreen" >
<meta-data android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.ImageGridActivity" />
</activity> </activity>
<activity <activity
android:name=".ui.ImageGridActivity" android:name=".ui.ImageGridActivity"

View File

@@ -11,4 +11,4 @@
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target. # Project target.
target=android-15 target=android-16

View File

@@ -0,0 +1,31 @@
<!--
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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape>
<solid android:color="@color/grid_state_pressed" />
</shape>
</item>
<item android:state_focused="true">
<shape>
<solid android:color="@color/grid_state_focused" />
</shape>
</item>
<item android:drawable="@android:color/transparent" />
</selector>

View File

@@ -16,12 +16,10 @@
--> -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/frameLayout"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" > android:layout_height="fill_parent" >
<ProgressBar <ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge" style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -1,4 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android" > <menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item <item

View File

@@ -17,7 +17,7 @@
<resources> <resources>
<dimen name="image_thumbnail_size">150dp</dimen> <dimen name="image_thumbnail_size">148dp</dimen>
<dimen name="image_thumbnail_spacing">1dp</dimen> <dimen name="image_thumbnail_spacing">2dp</dimen>
</resources> </resources>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<resources>
<color name="grid_state_pressed">#BB7dbcd3</color>
<color name="grid_state_focused">#777dbcd3</color>
</resources>

View File

@@ -19,12 +19,13 @@
<string name="app_name">BitmapFun</string> <string name="app_name">BitmapFun</string>
<string name="app_description">This is a sample application for the Android Training class <string name="app_description">This is a sample application for the Android Training class
"Displaying Bitmaps Efficiently" &quot;Displaying Bitmaps Efficiently&quot;
(http://developer.android.com/training/displaying-bitmaps/display-bitmap.html). It is not (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 designed to be a full reference application but to demonstrate the concepts discussed in
training course.</string> training course.</string>
<string name="clear_cache_menu">Clear Caches</string> <string name="clear_cache_menu">Clear Caches</string>
<string name="clear_cache_complete">Caches have been cleared</string> <string name="clear_cache_complete_toast">Caches have been cleared</string>
<string name="imageview_description">Image Thumbnail</string> <string name="imageview_description">Image Thumbnail</string>
<string name="no_network_connection_toast">No network connection found</string>
</resources> </resources>

View File

@@ -22,8 +22,8 @@
<style name="AppTheme.FullScreen" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen" /> <style name="AppTheme.FullScreen" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen" />
<style name="PhotoGridLayout"> <style name="PhotoGridLayout">
<item name="android:drawSelectorOnTop">false</item> <item name="android:drawSelectorOnTop">true</item>
<item name="android:listSelector">@null</item> <item name="android:listSelector">@drawable/photogrid_list_selector</item>
</style> </style>
</resources> </resources>

View File

@@ -16,8 +16,6 @@
package com.example.android.bitmapfun.provider; package com.example.android.bitmapfun.provider;
import com.example.android.bitmapfun.util.ImageWorker.ImageWorkerAdapter;
/** /**
* Some simple test data to use for this sample app. * Some simple test data to use for this sample app.
*/ */
@@ -28,45 +26,45 @@ public class Images {
* used to fetch the URLs. * used to fetch the URLs.
*/ */
public final static String[] imageUrls = new String[] { public final static String[] imageUrls = new String[] {
"https://lh6.googleusercontent.com/-jZgveEqb6pg/T3R4kXScycI/AAAAAAAAAE0/xQ7CvpfXDzc/s1024/sample_image_01.jpg", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://lh6.googleusercontent.com/-ZWHiPehwjTI/T3R41zuaKCI/AAAAAAAAAJg/hR3QJ1v3REg/s1024/sample_image_39.jpg",
}; };
/** /**
@@ -74,74 +72,44 @@ public class Images {
* should be used to fetch the URLs. * should be used to fetch the URLs.
*/ */
public final static String[] imageThumbUrls = new String[] { public final static String[] imageThumbUrls = new String[] {
"https://lh6.googleusercontent.com/-jZgveEqb6pg/T3R4kXScycI/AAAAAAAAAE0/xQ7CvpfXDzc/s160-c/sample_image_01.jpg", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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", "http://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;
}
}; };
} }

View File

@@ -16,32 +16,28 @@
package com.example.android.bitmapfun.ui; package com.example.android.bitmapfun.ui;
import android.annotation.SuppressLint; import android.annotation.TargetApi;
import android.app.ActionBar; import android.app.ActionBar;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.app.NavUtils;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.WindowManager.LayoutParams; import android.view.WindowManager.LayoutParams;
import android.widget.Toast; import android.widget.Toast;
import com.example.android.bitmapfun.BuildConfig;
import com.example.android.bitmapfun.R; import com.example.android.bitmapfun.R;
import com.example.android.bitmapfun.provider.Images; 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;
import com.example.android.bitmapfun.util.ImageFetcher; 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; import com.example.android.bitmapfun.util.Utils;
public class ImageDetailActivity extends FragmentActivity implements OnClickListener { public class ImageDetailActivity extends FragmentActivity implements OnClickListener {
@@ -49,51 +45,59 @@ public class ImageDetailActivity extends FragmentActivity implements OnClickList
public static final String EXTRA_IMAGE = "extra_image"; public static final String EXTRA_IMAGE = "extra_image";
private ImagePagerAdapter mAdapter; private ImagePagerAdapter mAdapter;
private ImageResizer mImageWorker; private ImageFetcher mImageFetcher;
private ViewPager mPager; private ViewPager mPager;
@SuppressLint("NewApi") @TargetApi(11)
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
if (BuildConfig.DEBUG) {
Utils.enableStrictMode();
}
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.image_detail_pager); setContentView(R.layout.image_detail_pager);
// Fetch screen height and width, to use as our max size when loading images as this // Fetch screen height and width, to use as our max size when loading images as this
// activity runs full screen // activity runs full screen
final DisplayMetrics displaymetrics = new DisplayMetrics(); final DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displaymetrics); getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
final int height = displaymetrics.heightPixels; final int height = displayMetrics.heightPixels;
final int width = displaymetrics.widthPixels; final int width = displayMetrics.widthPixels;
final int longest = height > width ? height : width;
// The ImageWorker takes care of loading images into our ImageView children asynchronously // For this sample we'll use half of the longest width to resize our images. As the
mImageWorker = new ImageFetcher(this, longest); // image scaling ensures the image is larger than this, we should be left with a
mImageWorker.setAdapter(Images.imageWorkerUrlsAdapter); // resolution that is appropriate for both portrait and landscape. For best image quality
mImageWorker.setImageCache(ImageCache.findOrCreateCache(this, IMAGE_CACHE_DIR)); // we shouldn't divide by 2, but this will use more memory and require a larger memory
mImageWorker.setImageFadeIn(false); // cache.
final int longest = (height > width ? height : width) / 2;
ImageCache.ImageCacheParams cacheParams =
new ImageCache.ImageCacheParams(this, IMAGE_CACHE_DIR);
cacheParams.setMemCacheSizePercent(this, 0.25f); // Set memory cache to 25% of mem class
// The ImageFetcher takes care of loading images into our ImageView children asynchronously
mImageFetcher = new ImageFetcher(this, longest);
mImageFetcher.addImageCache(getSupportFragmentManager(), cacheParams);
mImageFetcher.setImageFadeIn(false);
// Set up ViewPager and backing adapter // Set up ViewPager and backing adapter
mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), Images.imageUrls.length);
mImageWorker.getAdapter().getSize());
mPager = (ViewPager) findViewById(R.id.pager); mPager = (ViewPager) findViewById(R.id.pager);
mPager.setAdapter(mAdapter); mPager.setAdapter(mAdapter);
mPager.setPageMargin((int) getResources().getDimension(R.dimen.image_detail_pager_margin)); mPager.setPageMargin((int) getResources().getDimension(R.dimen.image_detail_pager_margin));
mPager.setOffscreenPageLimit(2);
// Set up activity to go full screen // Set up activity to go full screen
getWindow().addFlags(LayoutParams.FLAG_FULLSCREEN); getWindow().addFlags(LayoutParams.FLAG_FULLSCREEN);
// Enable some additional newer visibility and ActionBar features to create a more immersive // Enable some additional newer visibility and ActionBar features to create a more
// photo viewing experience // immersive photo viewing experience
if (Utils.hasActionBar()) { if (Utils.hasHoneycomb()) {
final ActionBar actionBar = getActionBar(); final ActionBar actionBar = getActionBar();
// Enable "up" navigation on ActionBar icon and hide title text // Hide title text and set home as up
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowTitleEnabled(false); actionBar.setDisplayShowTitleEnabled(false);
actionBar.setDisplayHomeAsUpEnabled(true);
// 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 // Hide and show the ActionBar as the visibility changes
mPager.setOnSystemUiVisibilityChangeListener( mPager.setOnSystemUiVisibilityChangeListener(
@@ -107,6 +111,10 @@ public class ImageDetailActivity extends FragmentActivity implements OnClickList
} }
} }
}); });
// Start low profile mode and hide ActionBar
mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
actionBar.hide();
} }
// Set the current item based on the extra passed in to this activity // Set the current item based on the extra passed in to this activity
@@ -116,23 +124,35 @@ public class ImageDetailActivity extends FragmentActivity implements OnClickList
} }
} }
@Override
public void onResume() {
super.onResume();
mImageFetcher.setExitTasksEarly(false);
}
@Override
protected void onPause() {
super.onPause();
mImageFetcher.setExitTasksEarly(true);
mImageFetcher.flushCache();
}
@Override
protected void onDestroy() {
super.onDestroy();
mImageFetcher.closeCache();
}
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case android.R.id.home: case android.R.id.home:
// Home or "up" navigation NavUtils.navigateUpFromSameTask(this);
final Intent intent = new Intent(this, ImageGridActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true; return true;
case R.id.clear_cache: case R.id.clear_cache:
final ImageCache cache = mImageWorker.getImageCache(); mImageFetcher.clearCache();
if (cache != null) { Toast.makeText(
mImageWorker.getImageCache().clearCaches(); this, R.string.clear_cache_complete_toast,Toast.LENGTH_SHORT).show();
DiskLruCache.clearCache(this, ImageFetcher.HTTP_CACHE_DIR);
Toast.makeText(this, R.string.clear_cache_complete,
Toast.LENGTH_SHORT).show();
}
return true; return true;
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
@@ -140,18 +160,15 @@ public class ImageDetailActivity extends FragmentActivity implements OnClickList
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater(); getMenuInflater().inflate(R.menu.main_menu, menu);
inflater.inflate(R.menu.main_menu, menu);
return true; return true;
} }
/** /**
* Called by the ViewPager child fragments to load images via the one ImageWorker * Called by the ViewPager child fragments to load images via the one ImageFetcher
*
* @return
*/ */
public ImageWorker getImageWorker() { public ImageFetcher getImageFetcher() {
return mImageWorker; return mImageFetcher;
} }
/** /**
@@ -174,15 +191,7 @@ public class ImageDetailActivity extends FragmentActivity implements OnClickList
@Override @Override
public Fragment getItem(int position) { public Fragment getItem(int position) {
return ImageDetailFragment.newInstance(position); return ImageDetailFragment.newInstance(Images.imageUrls[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);
} }
} }
@@ -190,7 +199,7 @@ public class ImageDetailActivity extends FragmentActivity implements OnClickList
* Set on the ImageView in the ViewPager children fragments, to enable/disable low profile mode * Set on the ImageView in the ViewPager children fragments, to enable/disable low profile mode
* when the ImageView is touched. * when the ImageView is touched.
*/ */
@SuppressLint("NewApi") @TargetApi(11)
@Override @Override
public void onClick(View v) { public void onClick(View v) {
final int vis = mPager.getSystemUiVisibility(); final int vis = mPager.getSystemUiVisibility();

View File

@@ -18,6 +18,7 @@ package com.example.android.bitmapfun.ui;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
@@ -25,6 +26,7 @@ import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import com.example.android.bitmapfun.R; import com.example.android.bitmapfun.R;
import com.example.android.bitmapfun.util.ImageFetcher;
import com.example.android.bitmapfun.util.ImageWorker; import com.example.android.bitmapfun.util.ImageWorker;
import com.example.android.bitmapfun.util.Utils; import com.example.android.bitmapfun.util.Utils;
@@ -32,22 +34,22 @@ import com.example.android.bitmapfun.util.Utils;
* This fragment will populate the children of the ViewPager from {@link ImageDetailActivity}. * This fragment will populate the children of the ViewPager from {@link ImageDetailActivity}.
*/ */
public class ImageDetailFragment extends Fragment { public class ImageDetailFragment extends Fragment {
private static final String IMAGE_DATA_EXTRA = "resId"; private static final String IMAGE_DATA_EXTRA = "extra_image_data";
private int mImageNum; private String mImageUrl;
private ImageView mImageView; private ImageView mImageView;
private ImageWorker mImageWorker; private ImageFetcher mImageFetcher;
/** /**
* Factory method to generate a new instance of the fragment given an image number. * 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 * @param imageUrl The image url to load
* @return A new instance of ImageDetailFragment with imageNum extras * @return A new instance of ImageDetailFragment with imageNum extras
*/ */
public static ImageDetailFragment newInstance(int imageNum) { public static ImageDetailFragment newInstance(String imageUrl) {
final ImageDetailFragment f = new ImageDetailFragment(); final ImageDetailFragment f = new ImageDetailFragment();
final Bundle args = new Bundle(); final Bundle args = new Bundle();
args.putInt(IMAGE_DATA_EXTRA, imageNum); args.putString(IMAGE_DATA_EXTRA, imageUrl);
f.setArguments(args); f.setArguments(args);
return f; return f;
@@ -59,13 +61,13 @@ public class ImageDetailFragment extends Fragment {
public ImageDetailFragment() {} public ImageDetailFragment() {}
/** /**
* Populate image number from extra, use the convenience factory method * Populate image using a url from extras, use the convenience factory method
* {@link ImageDetailFragment#newInstance(int)} to create this fragment. * {@link ImageDetailFragment#newInstance(String)} to create this fragment.
*/ */
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1; mImageUrl = getArguments() != null ? getArguments().getString(IMAGE_DATA_EXTRA) : null;
} }
@Override @Override
@@ -84,23 +86,23 @@ public class ImageDetailFragment extends Fragment {
// Use the parent activity to load the image asynchronously into the ImageView (so a single // 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 // cache can be used over all pages in the ViewPager
if (ImageDetailActivity.class.isInstance(getActivity())) { if (ImageDetailActivity.class.isInstance(getActivity())) {
mImageWorker = ((ImageDetailActivity) getActivity()).getImageWorker(); mImageFetcher = ((ImageDetailActivity) getActivity()).getImageFetcher();
mImageWorker.loadImage(mImageNum, mImageView); mImageFetcher.loadImage(mImageUrl, mImageView);
} }
// Pass clicks on the ImageView to the parent activity to handle // Pass clicks on the ImageView to the parent activity to handle
if (OnClickListener.class.isInstance(getActivity()) && Utils.hasActionBar()) { if (OnClickListener.class.isInstance(getActivity()) && Utils.hasHoneycomb()) {
mImageView.setOnClickListener((OnClickListener) getActivity()); mImageView.setOnClickListener((OnClickListener) getActivity());
} }
} }
/** @Override
* Cancels the asynchronous work taking place on the ImageView, called by the adapter backing public void onDestroy() {
* the ViewPager when the child is destroyed. super.onDestroy();
*/ if (mImageView != null) {
public void cancelWork() { // Cancel any pending image work
ImageWorker.cancelWork(mImageView); ImageWorker.cancelWork(mImageView);
mImageView.setImageDrawable(null); mImageView.setImageDrawable(null);
mImageView = null; }
} }
} }

View File

@@ -16,6 +16,9 @@
package com.example.android.bitmapfun.ui; package com.example.android.bitmapfun.ui;
import com.example.android.bitmapfun.BuildConfig;
import com.example.android.bitmapfun.util.Utils;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTransaction; import android.support.v4.app.FragmentTransaction;
@@ -24,10 +27,13 @@ import android.support.v4.app.FragmentTransaction;
* Simple FragmentActivity to hold the main {@link ImageGridFragment} and not much else. * Simple FragmentActivity to hold the main {@link ImageGridFragment} and not much else.
*/ */
public class ImageGridActivity extends FragmentActivity { public class ImageGridActivity extends FragmentActivity {
private static final String TAG = "ImageGridFragment"; private static final String TAG = "ImageGridActivity";
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
if (BuildConfig.DEBUG) {
Utils.enableStrictMode();
}
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (getSupportFragmentManager().findFragmentByTag(TAG) == null) { if (getSupportFragmentManager().findFragmentByTag(TAG) == null) {

View File

@@ -16,6 +16,8 @@
package com.example.android.bitmapfun.ui; package com.example.android.bitmapfun.ui;
import android.annotation.TargetApi;
import android.app.ActivityOptions;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
@@ -40,11 +42,8 @@ import android.widget.Toast;
import com.example.android.bitmapfun.BuildConfig; import com.example.android.bitmapfun.BuildConfig;
import com.example.android.bitmapfun.R; import com.example.android.bitmapfun.R;
import com.example.android.bitmapfun.provider.Images; 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.ImageCache.ImageCacheParams;
import com.example.android.bitmapfun.util.ImageFetcher; import com.example.android.bitmapfun.util.ImageFetcher;
import com.example.android.bitmapfun.util.ImageResizer;
import com.example.android.bitmapfun.util.Utils; import com.example.android.bitmapfun.util.Utils;
/** /**
@@ -52,7 +51,7 @@ import com.example.android.bitmapfun.util.Utils;
* implementation with the key addition being the ImageWorker class w/ImageCache to load children * 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 * 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 * cache is retained over configuration changes like orientation change so the images are populated
* quickly as the user rotates the device. * quickly if, for example, the user rotates the device.
*/ */
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener { public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
private static final String TAG = "ImageGridFragment"; private static final String TAG = "ImageGridFragment";
@@ -61,7 +60,7 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
private int mImageThumbSize; private int mImageThumbSize;
private int mImageThumbSpacing; private int mImageThumbSpacing;
private ImageAdapter mAdapter; private ImageAdapter mAdapter;
private ImageResizer mImageWorker; private ImageFetcher mImageFetcher;
/** /**
* Empty constructor as per the Fragment documentation * Empty constructor as per the Fragment documentation
@@ -78,22 +77,15 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
mAdapter = new ImageAdapter(getActivity()); mAdapter = new ImageAdapter(getActivity());
ImageCacheParams cacheParams = new ImageCacheParams(IMAGE_CACHE_DIR); ImageCacheParams cacheParams = new ImageCacheParams(getActivity(), IMAGE_CACHE_DIR);
// Allocate a third of the per-app memory limit to the bitmap memory cache. This value // Set memory cache to 25% of mem class
// should be chosen carefully based on a number of factors. Refer to the corresponding cacheParams.setMemCacheSizePercent(getActivity(), 0.25f);
// 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 // The ImageFetcher takes care of loading images into our ImageView children asynchronously
mImageWorker = new ImageFetcher(getActivity(), mImageThumbSize); mImageFetcher = new ImageFetcher(getActivity(), mImageThumbSize);
mImageWorker.setAdapter(Images.imageThumbWorkerUrlsAdapter); mImageFetcher.setLoadingImage(R.drawable.empty_photo);
mImageWorker.setLoadingImage(R.drawable.empty_photo); mImageFetcher.addImageCache(getActivity().getSupportFragmentManager(), cacheParams);
mImageWorker.setImageCache(ImageCache.findOrCreateCache(getActivity(), cacheParams));
} }
@Override @Override
@@ -104,6 +96,22 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
final GridView mGridView = (GridView) v.findViewById(R.id.gridView); final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
mGridView.setAdapter(mAdapter); mGridView.setAdapter(mAdapter);
mGridView.setOnItemClickListener(this); mGridView.setOnItemClickListener(this);
mGridView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView absListView, int scrollState) {
// Pause fetcher to ensure smoother scrolling when flinging
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
mImageFetcher.setPauseWork(true);
} else {
mImageFetcher.setPauseWork(false);
}
}
@Override
public void onScroll(AbsListView absListView, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
}
});
// This listener is used to get the final width of the GridView and then calculate the // 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 // number of columns and the width of each column. The width of each column is variable
@@ -135,22 +143,39 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
mImageWorker.setExitTasksEarly(false); mImageFetcher.setExitTasksEarly(false);
mAdapter.notifyDataSetChanged(); mAdapter.notifyDataSetChanged();
} }
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
mImageWorker.setExitTasksEarly(true); mImageFetcher.setExitTasksEarly(true);
mImageFetcher.flushCache();
} }
@Override
public void onDestroy() {
super.onDestroy();
mImageFetcher.closeCache();
}
@TargetApi(16)
@Override @Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) { public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
final Intent i = new Intent(getActivity(), ImageDetailActivity.class); final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
i.putExtra(ImageDetailActivity.EXTRA_IMAGE, (int) id); i.putExtra(ImageDetailActivity.EXTRA_IMAGE, (int) id);
if (Utils.hasJellyBean()) {
// makeThumbnailScaleUpAnimation() looks kind of ugly here as the loading spinner may
// show plus the thumbnail image in GridView is cropped. so using
// makeScaleUpAnimation() instead.
ActivityOptions options =
ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getWidth(), v.getHeight());
getActivity().startActivity(i, options.toBundle());
} else {
startActivity(i); startActivity(i);
} }
}
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
@@ -161,13 +186,9 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.clear_cache: case R.id.clear_cache:
final ImageCache cache = mImageWorker.getImageCache(); mImageFetcher.clearCache();
if (cache != null) { Toast.makeText(getActivity(), R.string.clear_cache_complete_toast,
mImageWorker.getImageCache().clearCaches();
DiskLruCache.clearCache(getActivity(), ImageFetcher.HTTP_CACHE_DIR);
Toast.makeText(getActivity(), R.string.clear_cache_complete,
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
}
return true; return true;
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
@@ -183,7 +204,7 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
private final Context mContext; private final Context mContext;
private int mItemHeight = 0; private int mItemHeight = 0;
private int mNumColumns = 0; private int mNumColumns = 0;
private int mActionBarHeight = -1; private int mActionBarHeight = 0;
private GridView.LayoutParams mImageViewLayoutParams; private GridView.LayoutParams mImageViewLayoutParams;
public ImageAdapter(Context context) { public ImageAdapter(Context context) {
@@ -191,18 +212,25 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
mContext = context; mContext = context;
mImageViewLayoutParams = new GridView.LayoutParams( mImageViewLayoutParams = new GridView.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
// Calculate ActionBar height
TypedValue tv = new TypedValue();
if (context.getTheme().resolveAttribute(
android.R.attr.actionBarSize, tv, true)) {
mActionBarHeight = TypedValue.complexToDimensionPixelSize(
tv.data, context.getResources().getDisplayMetrics());
}
} }
@Override @Override
public int getCount() { public int getCount() {
// Size of adapter + number of columns for top empty row // Size + number of columns for top empty row
return mImageWorker.getAdapter().getSize() + mNumColumns; return Images.imageThumbUrls.length + mNumColumns;
} }
@Override @Override
public Object getItem(int position) { public Object getItem(int position) {
return position < mNumColumns ? return position < mNumColumns ?
null : mImageWorker.getAdapter().getItem(position - mNumColumns); null : Images.imageThumbUrls[position - mNumColumns];
} }
@Override @Override
@@ -233,18 +261,6 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
if (convertView == null) { if (convertView == null) {
convertView = new View(mContext); 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 // Set empty view with height of ActionBar
convertView.setLayoutParams(new AbsListView.LayoutParams( convertView.setLayoutParams(new AbsListView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, mActionBarHeight)); ViewGroup.LayoutParams.MATCH_PARENT, mActionBarHeight));
@@ -268,7 +284,7 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
// Finally load the image asynchronously into the ImageView, this also takes care of // Finally load the image asynchronously into the ImageView, this also takes care of
// setting a placeholder image while the background thread runs // setting a placeholder image while the background thread runs
mImageWorker.loadImage(position - mNumColumns, imageView); mImageFetcher.loadImage(Images.imageThumbUrls[position - mNumColumns], imageView);
return imageView; return imageView;
} }
@@ -285,7 +301,7 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
mItemHeight = height; mItemHeight = height;
mImageViewLayoutParams = mImageViewLayoutParams =
new GridView.LayoutParams(LayoutParams.MATCH_PARENT, mItemHeight); new GridView.LayoutParams(LayoutParams.MATCH_PARENT, mItemHeight);
mImageWorker.setImageSize(height); mImageFetcher.setImageSize(height);
notifyDataSetChanged(); notifyDataSetChanged();
} }

View File

@@ -0,0 +1,693 @@
/*
* Copyright (C) 2008 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.TargetApi;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
import java.util.ArrayDeque;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* *************************************
* Copied from JB release framework:
* https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/os/AsyncTask.java
*
* so that threading behavior on all OS versions is the same and we can tweak behavior by using
* executeOnExecutor() if needed.
*
* There are 3 changes in this copy of AsyncTask:
* -pre-HC a single thread executor is used for serial operation
* (Executors.newSingleThreadExecutor) and is the default
* -the default THREAD_POOL_EXECUTOR was changed to use DiscardOldestPolicy
* -a new fixed thread pool called DUAL_THREAD_EXECUTOR was added
* *************************************
*
* <p>AsyncTask enables proper and easy use of the UI thread. This class allows to
* perform background operations and publish results on the UI thread without
* having to manipulate threads and/or handlers.</p>
*
* <p>AsyncTask is designed to be a helper class around {@link Thread} and {@link Handler}
* and does not constitute a generic threading framework. AsyncTasks should ideally be
* used for short operations (a few seconds at the most.) If you need to keep threads
* running for long periods of time, it is highly recommended you use the various APIs
* provided by the <code>java.util.concurrent</code> pacakge such as {@link Executor},
* {@link ThreadPoolExecutor} and {@link FutureTask}.</p>
*
* <p>An asynchronous task is defined by a computation that runs on a background thread and
* whose result is published on the UI thread. An asynchronous task is defined by 3 generic
* types, called <code>Params</code>, <code>Progress</code> and <code>Result</code>,
* and 4 steps, called <code>onPreExecute</code>, <code>doInBackground</code>,
* <code>onProgressUpdate</code> and <code>onPostExecute</code>.</p>
*
* <div class="special reference">
* <h3>Developer Guides</h3>
* <p>For more information about using tasks and threads, read the
* <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html">Processes and
* Threads</a> developer guide.</p>
* </div>
*
* <h2>Usage</h2>
* <p>AsyncTask must be subclassed to be used. The subclass will override at least
* one method ({@link #doInBackground}), and most often will override a
* second one ({@link #onPostExecute}.)</p>
*
* <p>Here is an example of subclassing:</p>
* <pre class="prettyprint">
* private class DownloadFilesTask extends AsyncTask&lt;URL, Integer, Long&gt; {
* protected Long doInBackground(URL... urls) {
* int count = urls.length;
* long totalSize = 0;
* for (int i = 0; i < count; i++) {
* totalSize += Downloader.downloadFile(urls[i]);
* publishProgress((int) ((i / (float) count) * 100));
* // Escape early if cancel() is called
* if (isCancelled()) break;
* }
* return totalSize;
* }
*
* protected void onProgressUpdate(Integer... progress) {
* setProgressPercent(progress[0]);
* }
*
* protected void onPostExecute(Long result) {
* showDialog("Downloaded " + result + " bytes");
* }
* }
* </pre>
*
* <p>Once created, a task is executed very simply:</p>
* <pre class="prettyprint">
* new DownloadFilesTask().execute(url1, url2, url3);
* </pre>
*
* <h2>AsyncTask's generic types</h2>
* <p>The three types used by an asynchronous task are the following:</p>
* <ol>
* <li><code>Params</code>, the type of the parameters sent to the task upon
* execution.</li>
* <li><code>Progress</code>, the type of the progress units published during
* the background computation.</li>
* <li><code>Result</code>, the type of the result of the background
* computation.</li>
* </ol>
* <p>Not all types are always used by an asynchronous task. To mark a type as unused,
* simply use the type {@link Void}:</p>
* <pre>
* private class MyTask extends AsyncTask&lt;Void, Void, Void&gt; { ... }
* </pre>
*
* <h2>The 4 steps</h2>
* <p>When an asynchronous task is executed, the task goes through 4 steps:</p>
* <ol>
* <li>{@link #onPreExecute()}, invoked on the UI thread immediately after the task
* is executed. This step is normally used to setup the task, for instance by
* showing a progress bar in the user interface.</li>
* <li>{@link #doInBackground}, invoked on the background thread
* immediately after {@link #onPreExecute()} finishes executing. This step is used
* to perform background computation that can take a long time. The parameters
* of the asynchronous task are passed to this step. The result of the computation must
* be returned by this step and will be passed back to the last step. This step
* can also use {@link #publishProgress} to publish one or more units
* of progress. These values are published on the UI thread, in the
* {@link #onProgressUpdate} step.</li>
* <li>{@link #onProgressUpdate}, invoked on the UI thread after a
* call to {@link #publishProgress}. The timing of the execution is
* undefined. This method is used to display any form of progress in the user
* interface while the background computation is still executing. For instance,
* it can be used to animate a progress bar or show logs in a text field.</li>
* <li>{@link #onPostExecute}, invoked on the UI thread after the background
* computation finishes. The result of the background computation is passed to
* this step as a parameter.</li>
* </ol>
*
* <h2>Cancelling a task</h2>
* <p>A task can be cancelled at any time by invoking {@link #cancel(boolean)}. Invoking
* this method will cause subsequent calls to {@link #isCancelled()} to return true.
* After invoking this method, {@link #onCancelled(Object)}, instead of
* {@link #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])}
* returns. To ensure that a task is cancelled as quickly as possible, you should always
* check the return value of {@link #isCancelled()} periodically from
* {@link #doInBackground(Object[])}, if possible (inside a loop for instance.)</p>
*
* <h2>Threading rules</h2>
* <p>There are a few threading rules that must be followed for this class to
* work properly:</p>
* <ul>
* <li>The AsyncTask class must be loaded on the UI thread. This is done
* automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.</li>
* <li>The task instance must be created on the UI thread.</li>
* <li>{@link #execute} must be invoked on the UI thread.</li>
* <li>Do not call {@link #onPreExecute()}, {@link #onPostExecute},
* {@link #doInBackground}, {@link #onProgressUpdate} manually.</li>
* <li>The task can be executed only once (an exception will be thrown if
* a second execution is attempted.)</li>
* </ul>
*
* <h2>Memory observability</h2>
* <p>AsyncTask guarantees that all callback calls are synchronized in such a way that the following
* operations are safe without explicit synchronizations.</p>
* <ul>
* <li>Set member fields in the constructor or {@link #onPreExecute}, and refer to them
* in {@link #doInBackground}.
* <li>Set member fields in {@link #doInBackground}, and refer to them in
* {@link #onProgressUpdate} and {@link #onPostExecute}.
* </ul>
*
* <h2>Order of execution</h2>
* <p>When first introduced, AsyncTasks were executed serially on a single background
* thread. Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed
* to a pool of threads allowing multiple tasks to operate in parallel. Starting with
* {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are executed on a single
* thread to avoid common application errors caused by parallel execution.</p>
* <p>If you truly want parallel execution, you can invoke
* {@link #executeOnExecutor(java.util.concurrent.Executor, Object[])} with
* {@link #THREAD_POOL_EXECUTOR}.</p>
*/
public abstract class AsyncTask<Params, Progress, Result> {
private static final String LOG_TAG = "AsyncTask";
private static final int CORE_POOL_SIZE = 5;
private static final int MAXIMUM_POOL_SIZE = 128;
private static final int KEEP_ALIVE = 1;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(10);
/**
* An {@link Executor} that can be used to execute tasks in parallel.
*/
public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory,
new ThreadPoolExecutor.DiscardOldestPolicy());
/**
* An {@link Executor} that executes tasks one at a time in serial
* order. This serialization is global to a particular process.
*/
public static final Executor SERIAL_EXECUTOR = Utils.hasHoneycomb() ? new SerialExecutor() :
Executors.newSingleThreadExecutor(sThreadFactory);
public static final Executor DUAL_THREAD_EXECUTOR =
Executors.newFixedThreadPool(2, sThreadFactory);
private static final int MESSAGE_POST_RESULT = 0x1;
private static final int MESSAGE_POST_PROGRESS = 0x2;
private static final InternalHandler sHandler = new InternalHandler();
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
private final WorkerRunnable<Params, Result> mWorker;
private final FutureTask<Result> mFuture;
private volatile Status mStatus = Status.PENDING;
private final AtomicBoolean mCancelled = new AtomicBoolean();
private final AtomicBoolean mTaskInvoked = new AtomicBoolean();
@TargetApi(11)
private static class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;
public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}
/**
* Indicates the current status of the task. Each status will be set only once
* during the lifetime of a task.
*/
public enum Status {
/**
* Indicates that the task has not been executed yet.
*/
PENDING,
/**
* Indicates that the task is running.
*/
RUNNING,
/**
* Indicates that {@link AsyncTask#onPostExecute} has finished.
*/
FINISHED,
}
/** @hide Used to force static handler to be created. */
public static void init() {
sHandler.getLooper();
}
/** @hide */
public static void setDefaultExecutor(Executor exec) {
sDefaultExecutor = exec;
}
/**
* Creates a new asynchronous task. This constructor must be invoked on the UI thread.
*/
public AsyncTask() {
mWorker = new WorkerRunnable<Params, Result>() {
public Result call() throws Exception {
mTaskInvoked.set(true);
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
return postResult(doInBackground(mParams));
}
};
mFuture = new FutureTask<Result>(mWorker) {
@Override
protected void done() {
try {
postResultIfNotInvoked(get());
} catch (InterruptedException e) {
android.util.Log.w(LOG_TAG, e);
} catch (ExecutionException e) {
throw new RuntimeException("An error occured while executing doInBackground()",
e.getCause());
} catch (CancellationException e) {
postResultIfNotInvoked(null);
}
}
};
}
private void postResultIfNotInvoked(Result result) {
final boolean wasTaskInvoked = mTaskInvoked.get();
if (!wasTaskInvoked) {
postResult(result);
}
}
private Result postResult(Result result) {
@SuppressWarnings("unchecked")
Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
new AsyncTaskResult<Result>(this, result));
message.sendToTarget();
return result;
}
/**
* Returns the current status of this task.
*
* @return The current status.
*/
public final Status getStatus() {
return mStatus;
}
/**
* Override this method to perform a computation on a background thread. The
* specified parameters are the parameters passed to {@link #execute}
* by the caller of this task.
*
* This method can call {@link #publishProgress} to publish updates
* on the UI thread.
*
* @param params The parameters of the task.
*
* @return A result, defined by the subclass of this task.
*
* @see #onPreExecute()
* @see #onPostExecute
* @see #publishProgress
*/
protected abstract Result doInBackground(Params... params);
/**
* Runs on the UI thread before {@link #doInBackground}.
*
* @see #onPostExecute
* @see #doInBackground
*/
protected void onPreExecute() {
}
/**
* <p>Runs on the UI thread after {@link #doInBackground}. The
* specified result is the value returned by {@link #doInBackground}.</p>
*
* <p>This method won't be invoked if the task was cancelled.</p>
*
* @param result The result of the operation computed by {@link #doInBackground}.
*
* @see #onPreExecute
* @see #doInBackground
* @see #onCancelled(Object)
*/
@SuppressWarnings({"UnusedDeclaration"})
protected void onPostExecute(Result result) {
}
/**
* Runs on the UI thread after {@link #publishProgress} is invoked.
* The specified values are the values passed to {@link #publishProgress}.
*
* @param values The values indicating progress.
*
* @see #publishProgress
* @see #doInBackground
*/
@SuppressWarnings({"UnusedDeclaration"})
protected void onProgressUpdate(Progress... values) {
}
/**
* <p>Runs on the UI thread after {@link #cancel(boolean)} is invoked and
* {@link #doInBackground(Object[])} has finished.</p>
*
* <p>The default implementation simply invokes {@link #onCancelled()} and
* ignores the result. If you write your own implementation, do not call
* <code>super.onCancelled(result)</code>.</p>
*
* @param result The result, if any, computed in
* {@link #doInBackground(Object[])}, can be null
*
* @see #cancel(boolean)
* @see #isCancelled()
*/
@SuppressWarnings({"UnusedParameters"})
protected void onCancelled(Result result) {
onCancelled();
}
/**
* <p>Applications should preferably override {@link #onCancelled(Object)}.
* This method is invoked by the default implementation of
* {@link #onCancelled(Object)}.</p>
*
* <p>Runs on the UI thread after {@link #cancel(boolean)} is invoked and
* {@link #doInBackground(Object[])} has finished.</p>
*
* @see #onCancelled(Object)
* @see #cancel(boolean)
* @see #isCancelled()
*/
protected void onCancelled() {
}
/**
* Returns <tt>true</tt> if this task was cancelled before it completed
* normally. If you are calling {@link #cancel(boolean)} on the task,
* the value returned by this method should be checked periodically from
* {@link #doInBackground(Object[])} to end the task as soon as possible.
*
* @return <tt>true</tt> if task was cancelled before it completed
*
* @see #cancel(boolean)
*/
public final boolean isCancelled() {
return mCancelled.get();
}
/**
* <p>Attempts to cancel execution of this task. This attempt will
* fail if the task has already completed, already been cancelled,
* or could not be cancelled for some other reason. If successful,
* and this task has not started when <tt>cancel</tt> is called,
* this task should never run. If the task has already started,
* then the <tt>mayInterruptIfRunning</tt> parameter determines
* whether the thread executing this task should be interrupted in
* an attempt to stop the task.</p>
*
* <p>Calling this method will result in {@link #onCancelled(Object)} being
* invoked on the UI thread after {@link #doInBackground(Object[])}
* returns. Calling this method guarantees that {@link #onPostExecute(Object)}
* is never invoked. After invoking this method, you should check the
* value returned by {@link #isCancelled()} periodically from
* {@link #doInBackground(Object[])} to finish the task as early as
* possible.</p>
*
* @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
* task should be interrupted; otherwise, in-progress tasks are allowed
* to complete.
*
* @return <tt>false</tt> if the task could not be cancelled,
* typically because it has already completed normally;
* <tt>true</tt> otherwise
*
* @see #isCancelled()
* @see #onCancelled(Object)
*/
public final boolean cancel(boolean mayInterruptIfRunning) {
mCancelled.set(true);
return mFuture.cancel(mayInterruptIfRunning);
}
/**
* Waits if necessary for the computation to complete, and then
* retrieves its result.
*
* @return The computed result.
*
* @throws CancellationException If the computation was cancelled.
* @throws ExecutionException If the computation threw an exception.
* @throws InterruptedException If the current thread was interrupted
* while waiting.
*/
public final Result get() throws InterruptedException, ExecutionException {
return mFuture.get();
}
/**
* Waits if necessary for at most the given time for the computation
* to complete, and then retrieves its result.
*
* @param timeout Time to wait before cancelling the operation.
* @param unit The time unit for the timeout.
*
* @return The computed result.
*
* @throws CancellationException If the computation was cancelled.
* @throws ExecutionException If the computation threw an exception.
* @throws InterruptedException If the current thread was interrupted
* while waiting.
* @throws TimeoutException If the wait timed out.
*/
public final Result get(long timeout, TimeUnit unit) throws InterruptedException,
ExecutionException, TimeoutException {
return mFuture.get(timeout, unit);
}
/**
* Executes the task with the specified parameters. The task returns
* itself (this) so that the caller can keep a reference to it.
*
* <p>Note: this function schedules the task on a queue for a single background
* thread or pool of threads depending on the platform version. When first
* introduced, AsyncTasks were executed serially on a single background thread.
* Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed
* to a pool of threads allowing multiple tasks to operate in parallel. Starting
* {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being
* executed on a single thread to avoid common application errors caused
* by parallel execution. If you truly want parallel execution, you can use
* the {@link #executeOnExecutor} version of this method
* with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings
* on its use.
*
* <p>This method must be invoked on the UI thread.
*
* @param params The parameters of the task.
*
* @return This instance of AsyncTask.
*
* @throws IllegalStateException If {@link #getStatus()} returns either
* {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
*
* @see #executeOnExecutor(java.util.concurrent.Executor, Object[])
* @see #execute(Runnable)
*/
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}
/**
* Executes the task with the specified parameters. The task returns
* itself (this) so that the caller can keep a reference to it.
*
* <p>This method is typically used with {@link #THREAD_POOL_EXECUTOR} to
* allow multiple tasks to run in parallel on a pool of threads managed by
* AsyncTask, however you can also use your own {@link Executor} for custom
* behavior.
*
* <p><em>Warning:</em> Allowing multiple tasks to run in parallel from
* a thread pool is generally <em>not</em> what one wants, because the order
* of their operation is not defined. For example, if these tasks are used
* to modify any state in common (such as writing a file due to a button click),
* there are no guarantees on the order of the modifications.
* Without careful work it is possible in rare cases for the newer version
* of the data to be over-written by an older one, leading to obscure data
* loss and stability issues. Such changes are best
* executed in serial; to guarantee such work is serialized regardless of
* platform version you can use this function with {@link #SERIAL_EXECUTOR}.
*
* <p>This method must be invoked on the UI thread.
*
* @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a
* convenient process-wide thread pool for tasks that are loosely coupled.
* @param params The parameters of the task.
*
* @return This instance of AsyncTask.
*
* @throws IllegalStateException If {@link #getStatus()} returns either
* {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
*
* @see #execute(Object[])
*/
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
Params... params) {
if (mStatus != Status.PENDING) {
switch (mStatus) {
case RUNNING:
throw new IllegalStateException("Cannot execute task:"
+ " the task is already running.");
case FINISHED:
throw new IllegalStateException("Cannot execute task:"
+ " the task has already been executed "
+ "(a task can be executed only once)");
}
}
mStatus = Status.RUNNING;
onPreExecute();
mWorker.mParams = params;
exec.execute(mFuture);
return this;
}
/**
* Convenience version of {@link #execute(Object...)} for use with
* a simple Runnable object. See {@link #execute(Object[])} for more
* information on the order of execution.
*
* @see #execute(Object[])
* @see #executeOnExecutor(java.util.concurrent.Executor, Object[])
*/
public static void execute(Runnable runnable) {
sDefaultExecutor.execute(runnable);
}
/**
* This method can be invoked from {@link #doInBackground} to
* publish updates on the UI thread while the background computation is
* still running. Each call to this method will trigger the execution of
* {@link #onProgressUpdate} on the UI thread.
*
* {@link #onProgressUpdate} will note be called if the task has been
* canceled.
*
* @param values The progress values to update the UI with.
*
* @see #onProgressUpdate
* @see #doInBackground
*/
protected final void publishProgress(Progress... values) {
if (!isCancelled()) {
sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
new AsyncTaskResult<Progress>(this, values)).sendToTarget();
}
}
private void finish(Result result) {
if (isCancelled()) {
onCancelled(result);
} else {
onPostExecute(result);
}
mStatus = Status.FINISHED;
}
private static class InternalHandler extends Handler {
@SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
@Override
public void handleMessage(Message msg) {
AsyncTaskResult result = (AsyncTaskResult) msg.obj;
switch (msg.what) {
case MESSAGE_POST_RESULT:
// There is only one result
result.mTask.finish(result.mData[0]);
break;
case MESSAGE_POST_PROGRESS:
result.mTask.onProgressUpdate(result.mData);
break;
}
}
}
private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
Params[] mParams;
}
@SuppressWarnings({"RawUseOfParameterizedType"})
private static class AsyncTaskResult<Data> {
final AsyncTask mTask;
final Data[] mData;
AsyncTaskResult(AsyncTask task, Data... data) {
mTask = task;
mData = data;
}
}
}

View File

@@ -16,16 +16,28 @@
package com.example.android.bitmapfun.util; package com.example.android.bitmapfun.util;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat; import android.graphics.Bitmap.CompressFormat;
import android.support.v4.app.FragmentActivity; import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Environment;
import android.os.StatFs;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.util.LruCache; import android.support.v4.util.LruCache;
import android.util.Log; import android.util.Log;
import com.example.android.bitmapfun.BuildConfig; import com.example.android.bitmapfun.BuildConfig;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/** /**
* This class holds our bitmap caches (memory and disk). * This class holds our bitmap caches (memory and disk).
@@ -42,23 +54,27 @@ public class ImageCache {
// Compression settings when writing images to disk cache // Compression settings when writing images to disk cache
private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG; private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG;
private static final int DEFAULT_COMPRESS_QUALITY = 70; private static final int DEFAULT_COMPRESS_QUALITY = 70;
private static final int DISK_CACHE_INDEX = 0;
// Constants to easily toggle various caches // Constants to easily toggle various caches
private static final boolean DEFAULT_MEM_CACHE_ENABLED = true; private static final boolean DEFAULT_MEM_CACHE_ENABLED = true;
private static final boolean DEFAULT_DISK_CACHE_ENABLED = true; private static final boolean DEFAULT_DISK_CACHE_ENABLED = true;
private static final boolean DEFAULT_CLEAR_DISK_CACHE_ON_START = false; private static final boolean DEFAULT_CLEAR_DISK_CACHE_ON_START = false;
private static final boolean DEFAULT_INIT_DISK_CACHE_ON_CREATE = false;
private DiskLruCache mDiskCache; private DiskLruCache mDiskLruCache;
private LruCache<String, Bitmap> mMemoryCache; private LruCache<String, Bitmap> mMemoryCache;
private ImageCacheParams mCacheParams;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
/** /**
* Creating a new ImageCache object using the specified parameters. * 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 * @param cacheParams The cache parameters to use to initialize the cache
*/ */
public ImageCache(Context context, ImageCacheParams cacheParams) { public ImageCache(ImageCacheParams cacheParams) {
init(context, cacheParams); init(cacheParams);
} }
/** /**
@@ -68,43 +84,29 @@ public class ImageCache {
* @param uniqueName A unique name that will be appended to the cache directory * @param uniqueName A unique name that will be appended to the cache directory
*/ */
public ImageCache(Context context, String uniqueName) { public ImageCache(Context context, String uniqueName) {
init(context, new ImageCacheParams(uniqueName)); init(new ImageCacheParams(context, 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 * 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}. * one is created using the supplied params and saved to a {@link RetainFragment}.
* *
* @param activity The calling {@link FragmentActivity} * @param fragmentManager The fragment manager to use when dealing with the retained fragment.
* @param cacheParams The cache parameters to use if creating the ImageCache * @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 * @return An existing retained ImageCache object or a new one if one did not exist
*/ */
public static ImageCache findOrCreateCache( public static ImageCache findOrCreateCache(
final FragmentActivity activity, ImageCacheParams cacheParams) { FragmentManager fragmentManager, ImageCacheParams cacheParams) {
// Search for, or create an instance of the non-UI RetainFragment // Search for, or create an instance of the non-UI RetainFragment
final RetainFragment mRetainFragment = RetainFragment.findOrCreateRetainFragment( final RetainFragment mRetainFragment = findOrCreateRetainFragment(fragmentManager);
activity.getSupportFragmentManager());
// See if we already have an ImageCache stored in RetainFragment // See if we already have an ImageCache stored in RetainFragment
ImageCache imageCache = (ImageCache) mRetainFragment.getObject(); ImageCache imageCache = (ImageCache) mRetainFragment.getObject();
// No existing ImageCache, create one and store it in RetainFragment // No existing ImageCache, create one and store it in RetainFragment
if (imageCache == null) { if (imageCache == null) {
imageCache = new ImageCache(activity, cacheParams); imageCache = new ImageCache(cacheParams);
mRetainFragment.setObject(imageCache); mRetainFragment.setObject(imageCache);
} }
@@ -114,36 +116,75 @@ public class ImageCache {
/** /**
* Initialize the cache, providing all parameters. * Initialize the cache, providing all parameters.
* *
* @param context The context to use
* @param cacheParams The cache parameters to initialize the cache * @param cacheParams The cache parameters to initialize the cache
*/ */
private void init(Context context, ImageCacheParams cacheParams) { private void init(ImageCacheParams cacheParams) {
final File diskCacheDir = DiskLruCache.getDiskCacheDir(context, cacheParams.uniqueName); mCacheParams = cacheParams;
// 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 // Set up memory cache
if (cacheParams.memoryCacheEnabled) { if (mCacheParams.memoryCacheEnabled) {
mMemoryCache = new LruCache<String, Bitmap>(cacheParams.memCacheSize) { if (BuildConfig.DEBUG) {
Log.d(TAG, "Memory cache created (size = " + mCacheParams.memCacheSize + ")");
}
mMemoryCache = new LruCache<String, Bitmap>(mCacheParams.memCacheSize) {
/** /**
* Measure item size in bytes rather than units which is more practical for a bitmap * Measure item size in bytes rather than units which is more practical
* cache * for a bitmap cache
*/ */
@Override @Override
protected int sizeOf(String key, Bitmap bitmap) { protected int sizeOf(String key, Bitmap bitmap) {
return Utils.getBitmapSize(bitmap); return getBitmapSize(bitmap);
} }
}; };
} }
// By default the disk cache is not initialized here as it should be initialized
// on a separate thread due to disk access.
if (cacheParams.initDiskCacheOnCreate) {
// Set up disk cache
initDiskCache();
}
} }
/**
* Initializes the disk cache. Note that this includes disk access so this should not be
* executed on the main/UI thread. By default an ImageCache does not initialize the disk
* cache when it is created, instead you should call initDiskCache() to initialize it on a
* background thread.
*/
public void initDiskCache() {
// Set up disk cache
synchronized (mDiskCacheLock) {
if (mDiskLruCache == null || mDiskLruCache.isClosed()) {
File diskCacheDir = mCacheParams.diskCacheDir;
if (mCacheParams.diskCacheEnabled && diskCacheDir != null) {
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > mCacheParams.diskCacheSize) {
try {
mDiskLruCache = DiskLruCache.open(
diskCacheDir, 1, 1, mCacheParams.diskCacheSize);
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache initialized");
}
} catch (final IOException e) {
mCacheParams.diskCacheDir = null;
Log.e(TAG, "initDiskCache - " + e);
}
}
}
}
mDiskCacheStarting = false;
mDiskCacheLock.notifyAll();
}
}
/**
* Adds a bitmap to both memory and disk cache.
* @param data Unique identifier for the bitmap to store
* @param bitmap The bitmap to store
*/
public void addBitmapToCache(String data, Bitmap bitmap) { public void addBitmapToCache(String data, Bitmap bitmap) {
if (data == null || bitmap == null) { if (data == null || bitmap == null) {
return; return;
@@ -154,9 +195,37 @@ public class ImageCache {
mMemoryCache.put(data, bitmap); mMemoryCache.put(data, bitmap);
} }
synchronized (mDiskCacheLock) {
// Add to disk cache // Add to disk cache
if (mDiskCache != null && !mDiskCache.containsKey(data)) { if (mDiskLruCache != null) {
mDiskCache.put(data, bitmap); final String key = hashKeyForDisk(data);
OutputStream out = null;
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot == null) {
final DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
out = editor.newOutputStream(DISK_CACHE_INDEX);
bitmap.compress(
mCacheParams.compressFormat, mCacheParams.compressQuality, out);
editor.commit();
out.close();
}
} else {
snapshot.getInputStream(DISK_CACHE_INDEX).close();
}
} catch (final IOException e) {
Log.e(TAG, "addBitmapToCache - " + e);
} catch (Exception e) {
Log.e(TAG, "addBitmapToCache - " + e);
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {}
}
}
} }
} }
@@ -186,32 +255,324 @@ public class ImageCache {
* @return The bitmap if found in cache, null otherwise * @return The bitmap if found in cache, null otherwise
*/ */
public Bitmap getBitmapFromDiskCache(String data) { public Bitmap getBitmapFromDiskCache(String data) {
if (mDiskCache != null) { final String key = hashKeyForDisk(data);
return mDiskCache.get(data); synchronized (mDiskCacheLock) {
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
InputStream inputStream = null;
try {
final DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache hit");
}
inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
if (inputStream != null) {
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
return bitmap;
}
}
} catch (final IOException e) {
Log.e(TAG, "getBitmapFromDiskCache - " + e);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {}
}
} }
return null; return null;
} }
}
public void clearCaches() { /**
mDiskCache.clearCache(); * Clears both the memory and disk cache associated with this ImageCache object. Note that
* this includes disk access so this should not be executed on the main/UI thread.
*/
public void clearCache() {
if (mMemoryCache != null) {
mMemoryCache.evictAll(); mMemoryCache.evictAll();
if (BuildConfig.DEBUG) {
Log.d(TAG, "Memory cache cleared");
}
}
synchronized (mDiskCacheLock) {
mDiskCacheStarting = true;
if (mDiskLruCache != null && !mDiskLruCache.isClosed()) {
try {
mDiskLruCache.delete();
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache cleared");
}
} catch (IOException e) {
Log.e(TAG, "clearCache - " + e);
}
mDiskLruCache = null;
initDiskCache();
}
}
}
/**
* Flushes the disk cache associated with this ImageCache object. Note that this includes
* disk access so this should not be executed on the main/UI thread.
*/
public void flush() {
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null) {
try {
mDiskLruCache.flush();
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache flushed");
}
} catch (IOException e) {
Log.e(TAG, "flush - " + e);
}
}
}
}
/**
* Closes the disk cache associated with this ImageCache object. Note that this includes
* disk access so this should not be executed on the main/UI thread.
*/
public void close() {
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null) {
try {
if (!mDiskLruCache.isClosed()) {
mDiskLruCache.close();
mDiskLruCache = null;
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache closed");
}
}
} catch (IOException e) {
Log.e(TAG, "close - " + e);
}
}
}
} }
/** /**
* A holder class that contains cache parameters. * A holder class that contains cache parameters.
*/ */
public static class ImageCacheParams { public static class ImageCacheParams {
public String uniqueName;
public int memCacheSize = DEFAULT_MEM_CACHE_SIZE; public int memCacheSize = DEFAULT_MEM_CACHE_SIZE;
public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE; public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE;
public File diskCacheDir;
public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT; public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT;
public int compressQuality = DEFAULT_COMPRESS_QUALITY; public int compressQuality = DEFAULT_COMPRESS_QUALITY;
public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED; public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED;
public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED; public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED;
public boolean clearDiskCacheOnStart = DEFAULT_CLEAR_DISK_CACHE_ON_START; public boolean clearDiskCacheOnStart = DEFAULT_CLEAR_DISK_CACHE_ON_START;
public boolean initDiskCacheOnCreate = DEFAULT_INIT_DISK_CACHE_ON_CREATE;
public ImageCacheParams(String uniqueName) { public ImageCacheParams(Context context, String uniqueName) {
this.uniqueName = uniqueName; diskCacheDir = getDiskCacheDir(context, uniqueName);
}
public ImageCacheParams(File diskCacheDir) {
this.diskCacheDir = diskCacheDir;
}
/**
* Sets the memory cache size based on a percentage of the device memory class.
* Eg. setting percent to 0.2 would set the memory cache to one fifth of the device memory
* class. Throws {@link IllegalArgumentException} if percent is < 0.05 or > .8.
*
* 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/
*
* @param context Context to use to fetch memory class
* @param percent Percent of memory class to use to size memory cache
*/
public void setMemCacheSizePercent(Context context, float percent) {
if (percent < 0.05f || percent > 0.8f) {
throw new IllegalArgumentException("setMemCacheSizePercent - percent must be "
+ "between 0.05 and 0.8 (inclusive)");
}
memCacheSize = Math.round(percent * getMemoryClass(context) * 1024 * 1024);
}
private static int getMemoryClass(Context context) {
return ((ActivityManager) context.getSystemService(
Context.ACTIVITY_SERVICE)).getMemoryClass();
} }
} }
/**
* Get a usable cache directory (external if available, internal otherwise).
*
* @param context The context to use
* @param uniqueName A unique directory name to append to the cache dir
* @return The cache dir
*/
public static File getDiskCacheDir(Context context, String uniqueName) {
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
// otherwise use internal cache dir
final String cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}
/**
* A hashing method that changes a string (like a URL) into a hash suitable for using as a
* disk filename.
*/
public static String hashKeyForDisk(String key) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
private static String bytesToHexString(byte[] bytes) {
// http://stackoverflow.com/questions/332079
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
/**
* Get the size in bytes of a bitmap.
* @param bitmap
* @return size in bytes
*/
@TargetApi(12)
public static int getBitmapSize(Bitmap bitmap) {
if (Utils.hasHoneycombMR1()) {
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.
*/
@TargetApi(9)
public static boolean isExternalStorageRemovable() {
if (Utils.hasGingerbread()) {
return Environment.isExternalStorageRemovable();
}
return true;
}
/**
* Get the external app cache directory.
*
* @param context The context to use
* @return The external cache dir
*/
@TargetApi(8)
public static File getExternalCacheDir(Context context) {
if (Utils.hasFroyo()) {
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
*/
@TargetApi(9)
public static long getUsableSpace(File path) {
if (Utils.hasGingerbread()) {
return path.getUsableSpace();
}
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
/**
* 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).commitAllowingStateLoss();
}
return mRetainFragment;
}
/**
* A simple non-UI Fragment that stores a single Object and is retained over configuration
* changes. It will be used to retain the ImageCache object.
*/
public static class RetainFragment extends Fragment {
private Object mObject;
/**
* Empty constructor as per the Fragment documentation
*/
public RetainFragment() {}
@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;
}
}
} }

View File

@@ -20,17 +20,20 @@ import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.os.Build;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import com.example.android.bitmapfun.BuildConfig; import com.example.android.bitmapfun.BuildConfig;
import com.example.android.bitmapfun.R;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.OutputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
@@ -40,7 +43,14 @@ import java.net.URL;
public class ImageFetcher extends ImageResizer { public class ImageFetcher extends ImageResizer {
private static final String TAG = "ImageFetcher"; private static final String TAG = "ImageFetcher";
private static final int HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10MB private static final int HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10MB
public static final String HTTP_CACHE_DIR = "http"; private static final String HTTP_CACHE_DIR = "http";
private static final int IO_BUFFER_SIZE = 8 * 1024;
private DiskLruCache mHttpDiskCache;
private File mHttpCacheDir;
private boolean mHttpDiskCacheStarting = true;
private final Object mHttpDiskCacheLock = new Object();
private static final int DISK_CACHE_INDEX = 0;
/** /**
* Initialize providing a target image width and height for the processing images. * Initialize providing a target image width and height for the processing images.
@@ -67,6 +77,90 @@ public class ImageFetcher extends ImageResizer {
private void init(Context context) { private void init(Context context) {
checkConnection(context); checkConnection(context);
mHttpCacheDir = ImageCache.getDiskCacheDir(context, HTTP_CACHE_DIR);
}
@Override
protected void initDiskCacheInternal() {
super.initDiskCacheInternal();
initHttpDiskCache();
}
private void initHttpDiskCache() {
if (!mHttpCacheDir.exists()) {
mHttpCacheDir.mkdirs();
}
synchronized (mHttpDiskCacheLock) {
if (ImageCache.getUsableSpace(mHttpCacheDir) > HTTP_CACHE_SIZE) {
try {
mHttpDiskCache = DiskLruCache.open(mHttpCacheDir, 1, 1, HTTP_CACHE_SIZE);
if (BuildConfig.DEBUG) {
Log.d(TAG, "HTTP cache initialized");
}
} catch (IOException e) {
mHttpDiskCache = null;
}
}
mHttpDiskCacheStarting = false;
mHttpDiskCacheLock.notifyAll();
}
}
@Override
protected void clearCacheInternal() {
super.clearCacheInternal();
synchronized (mHttpDiskCacheLock) {
if (mHttpDiskCache != null && !mHttpDiskCache.isClosed()) {
try {
mHttpDiskCache.delete();
if (BuildConfig.DEBUG) {
Log.d(TAG, "HTTP cache cleared");
}
} catch (IOException e) {
Log.e(TAG, "clearCacheInternal - " + e);
}
mHttpDiskCache = null;
mHttpDiskCacheStarting = true;
initHttpDiskCache();
}
}
}
@Override
protected void flushCacheInternal() {
super.flushCacheInternal();
synchronized (mHttpDiskCacheLock) {
if (mHttpDiskCache != null) {
try {
mHttpDiskCache.flush();
if (BuildConfig.DEBUG) {
Log.d(TAG, "HTTP cache flushed");
}
} catch (IOException e) {
Log.e(TAG, "flush - " + e);
}
}
}
}
@Override
protected void closeCacheInternal() {
super.closeCacheInternal();
synchronized (mHttpDiskCacheLock) {
if (mHttpDiskCache != null) {
try {
if (!mHttpDiskCache.isClosed()) {
mHttpDiskCache.close();
mHttpDiskCache = null;
if (BuildConfig.DEBUG) {
Log.d(TAG, "HTTP cache closed");
}
}
} catch (IOException e) {
Log.e(TAG, "closeCacheInternal - " + e);
}
}
}
} }
/** /**
@@ -79,7 +173,7 @@ public class ImageFetcher extends ImageResizer {
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
final NetworkInfo networkInfo = cm.getActiveNetworkInfo(); final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
if (networkInfo == null || !networkInfo.isConnectedOrConnecting()) { if (networkInfo == null || !networkInfo.isConnectedOrConnecting()) {
Toast.makeText(context, "No network connection found.", Toast.LENGTH_LONG).show(); Toast.makeText(context, R.string.no_network_connection_toast, Toast.LENGTH_LONG).show();
Log.e(TAG, "checkConnection - no connection found"); Log.e(TAG, "checkConnection - no connection found");
} }
} }
@@ -96,15 +190,65 @@ public class ImageFetcher extends ImageResizer {
Log.d(TAG, "processBitmap - " + data); Log.d(TAG, "processBitmap - " + data);
} }
// Download a bitmap, write it to a file final String key = ImageCache.hashKeyForDisk(data);
final File f = downloadBitmap(mContext, data); FileDescriptor fileDescriptor = null;
FileInputStream fileInputStream = null;
if (f != null) { DiskLruCache.Snapshot snapshot;
// Return a sampled down version synchronized (mHttpDiskCacheLock) {
return decodeSampledBitmapFromFile(f.toString(), mImageWidth, mImageHeight); // Wait for disk cache to initialize
while (mHttpDiskCacheStarting) {
try {
mHttpDiskCacheLock.wait();
} catch (InterruptedException e) {}
} }
return null; if (mHttpDiskCache != null) {
try {
snapshot = mHttpDiskCache.get(key);
if (snapshot == null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "processBitmap, not found in http cache, downloading...");
}
DiskLruCache.Editor editor = mHttpDiskCache.edit(key);
if (editor != null) {
if (downloadUrlToStream(data,
editor.newOutputStream(DISK_CACHE_INDEX))) {
editor.commit();
} else {
editor.abort();
}
}
snapshot = mHttpDiskCache.get(key);
}
if (snapshot != null) {
fileInputStream =
(FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
fileDescriptor = fileInputStream.getFD();
}
} catch (IOException e) {
Log.e(TAG, "processBitmap - " + e);
} catch (IllegalStateException e) {
Log.e(TAG, "processBitmap - " + e);
} finally {
if (fileDescriptor == null && fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {}
}
}
}
}
Bitmap bitmap = null;
if (fileDescriptor != null) {
bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth, mImageHeight);
}
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {}
}
return bitmap;
} }
@Override @Override
@@ -113,65 +257,54 @@ public class ImageFetcher extends ImageResizer {
} }
/** /**
* Download a bitmap from a URL, write it to a disk and return the File pointer. This * Download a bitmap from a URL and write the content to an output stream.
* implementation uses a simple disk cache.
* *
* @param context The context to use
* @param urlString The URL to fetch * @param urlString The URL to fetch
* @return A File pointing to the fetched bitmap * @return true if successful, false otherwise
*/ */
public static File downloadBitmap(Context context, String urlString) { public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
final File cacheDir = DiskLruCache.getDiskCacheDir(context, HTTP_CACHE_DIR); disableConnectionReuseIfNecessary();
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; HttpURLConnection urlConnection = null;
BufferedOutputStream out = null; BufferedOutputStream out = null;
BufferedInputStream in = null;
try { try {
final URL url = new URL(urlString); final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection(); urlConnection = (HttpURLConnection) url.openConnection();
final InputStream in = in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
new BufferedInputStream(urlConnection.getInputStream(), Utils.IO_BUFFER_SIZE); out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
out = new BufferedOutputStream(new FileOutputStream(cacheFile), Utils.IO_BUFFER_SIZE);
int b; int b;
while ((b = in.read()) != -1) { while ((b = in.read()) != -1) {
out.write(b); out.write(b);
} }
return true;
return cacheFile;
} catch (final IOException e) { } catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmap - " + e); Log.e(TAG, "Error in downloadBitmap - " + e);
} finally { } finally {
if (urlConnection != null) { if (urlConnection != null) {
urlConnection.disconnect(); urlConnection.disconnect();
} }
if (out != null) {
try { try {
if (out != null) {
out.close(); out.close();
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmap - " + e);
} }
if (in != null) {
in.close();
} }
} catch (final IOException e) {}
}
return false;
} }
return null; /**
* 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 (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {
System.setProperty("http.keepAlive", "false");
}
} }
} }

View File

@@ -24,13 +24,15 @@ import android.util.Log;
import com.example.android.bitmapfun.BuildConfig; import com.example.android.bitmapfun.BuildConfig;
import java.io.FileDescriptor;
/** /**
* A simple subclass of {@link ImageWorker} that resizes images from resources given a target width * 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 * and height. Useful for when the input images might be too large to simply load directly into
* memory. * memory.
*/ */
public class ImageResizer extends ImageWorker { public class ImageResizer extends ImageWorker {
private static final String TAG = "ImageWorker"; private static final String TAG = "ImageResizer";
protected int mImageWidth; protected int mImageWidth;
protected int mImageHeight; protected int mImageHeight;
@@ -88,8 +90,7 @@ public class ImageResizer extends ImageWorker {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Log.d(TAG, "processBitmap - " + resId); Log.d(TAG, "processBitmap - " + resId);
} }
return decodeSampledBitmapFromResource( return decodeSampledBitmapFromResource(mResources, resId, mImageWidth, mImageHeight);
mContext.getResources(), resId, mImageWidth, mImageHeight);
} }
@Override @Override
@@ -132,7 +133,7 @@ public class ImageResizer extends ImageWorker {
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions * @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 * that are equal to or greater than the requested width and height
*/ */
public static synchronized Bitmap decodeSampledBitmapFromFile(String filename, public static Bitmap decodeSampledBitmapFromFile(String filename,
int reqWidth, int reqHeight) { int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions // First decode with inJustDecodeBounds=true to check dimensions
@@ -148,6 +149,31 @@ public class ImageResizer extends ImageWorker {
return BitmapFactory.decodeFile(filename, options); return BitmapFactory.decodeFile(filename, options);
} }
/**
* Decode and sample down a bitmap from a file input stream to the requested width and height.
*
* @param fileDescriptor The file descriptor to read from
* @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 decodeSampledBitmapFromDescriptor(
FileDescriptor fileDescriptor, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
}
/** /**
* Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding * Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding
* bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates * bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates

View File

@@ -24,7 +24,7 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable; import android.graphics.drawable.TransitionDrawable;
import android.os.AsyncTask; import android.support.v4.app.FragmentManager;
import android.util.Log; import android.util.Log;
import android.widget.ImageView; import android.widget.ImageView;
@@ -42,15 +42,22 @@ public abstract class ImageWorker {
private static final int FADE_IN_TIME = 200; private static final int FADE_IN_TIME = 200;
private ImageCache mImageCache; private ImageCache mImageCache;
private ImageCache.ImageCacheParams mImageCacheParams;
private Bitmap mLoadingBitmap; private Bitmap mLoadingBitmap;
private boolean mFadeInBitmap = true; private boolean mFadeInBitmap = true;
private boolean mExitTasksEarly = false; private boolean mExitTasksEarly = false;
protected boolean mPauseWork = false;
private final Object mPauseWorkLock = new Object();
protected Context mContext; protected Resources mResources;
protected ImageWorkerAdapter mImageWorkerAdapter;
private static final int MESSAGE_CLEAR = 0;
private static final int MESSAGE_INIT_DISK_CACHE = 1;
private static final int MESSAGE_FLUSH = 2;
private static final int MESSAGE_CLOSE = 3;
protected ImageWorker(Context context) { protected ImageWorker(Context context) {
mContext = context; mResources = context.getResources();
} }
/** /**
@@ -65,6 +72,10 @@ public abstract class ImageWorker {
* @param imageView The ImageView to bind the downloaded image to. * @param imageView The ImageView to bind the downloaded image to.
*/ */
public void loadImage(Object data, ImageView imageView) { public void loadImage(Object data, ImageView imageView) {
if (data == null) {
return;
}
Bitmap bitmap = null; Bitmap bitmap = null;
if (mImageCache != null) { if (mImageCache != null) {
@@ -77,29 +88,13 @@ public abstract class ImageWorker {
} else if (cancelPotentialWork(data, imageView)) { } else if (cancelPotentialWork(data, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView); final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable = final AsyncDrawable asyncDrawable =
new AsyncDrawable(mContext.getResources(), mLoadingBitmap, task); new AsyncDrawable(mResources, mLoadingBitmap, task);
imageView.setImageDrawable(asyncDrawable); imageView.setImageDrawable(asyncDrawable);
task.execute(data);
}
}
/** // NOTE: This uses a custom version of AsyncTask that has been pulled from the
* Load an image specified from a set adapter into an ImageView (override // framework and slightly modified. Refer to the docs at the top of the class
* {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and disk // for more info on what was changed.
* cache will be used if an {@link ImageCache} has been set using task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR, data);
* {@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.");
} }
} }
@@ -118,26 +113,36 @@ public abstract class ImageWorker {
* @param resId * @param resId
*/ */
public void setLoadingImage(int resId) { public void setLoadingImage(int resId) {
mLoadingBitmap = BitmapFactory.decodeResource(mContext.getResources(), resId); mLoadingBitmap = BitmapFactory.decodeResource(mResources, resId);
} }
/** /**
* Set the {@link ImageCache} object to use with this ImageWorker. * Adds an {@link ImageCache} to this worker in the background (to prevent disk access on UI
* * thread).
* @param cacheCallback * @param fragmentManager
* @param cacheParams
*/ */
public void setImageCache(ImageCache cacheCallback) { public void addImageCache(FragmentManager fragmentManager,
mImageCache = cacheCallback; ImageCache.ImageCacheParams cacheParams) {
mImageCacheParams = cacheParams;
setImageCache(ImageCache.findOrCreateCache(fragmentManager, mImageCacheParams));
new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE);
} }
public ImageCache getImageCache() { /**
return mImageCache; * Sets the {@link ImageCache} object to use with this ImageWorker. Usually you will not need
* to call this directly, instead use {@link ImageWorker#addImageCache} which will create and
* add the {@link ImageCache} object in a background thread (to ensure no disk access on the
* main/UI thread).
*
* @param imageCache
*/
public void setImageCache(ImageCache imageCache) {
mImageCache = imageCache;
} }
/** /**
* If set to true, the image will fade-in once it has been loaded by the background thread. * 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) { public void setImageFadeIn(boolean fadeIn) {
mFadeInBitmap = fadeIn; mFadeInBitmap = fadeIn;
@@ -158,6 +163,10 @@ public abstract class ImageWorker {
*/ */
protected abstract Bitmap processBitmap(Object data); protected abstract Bitmap processBitmap(Object data);
/**
* Cancels any pending work attached to the provided ImageView.
* @param imageView
*/
public static void cancelWork(ImageView imageView) { public static void cancelWork(ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) { if (bitmapWorkerTask != null) {
@@ -225,10 +234,23 @@ public abstract class ImageWorker {
*/ */
@Override @Override
protected Bitmap doInBackground(Object... params) { protected Bitmap doInBackground(Object... params) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "doInBackground - starting work");
}
data = params[0]; data = params[0];
final String dataString = String.valueOf(data); final String dataString = String.valueOf(data);
Bitmap bitmap = null; Bitmap bitmap = null;
// Wait here if work is paused and the task is not cancelled
synchronized (mPauseWorkLock) {
while (mPauseWork && !isCancelled()) {
try {
mPauseWorkLock.wait();
} catch (InterruptedException e) {}
}
}
// If the image cache is available and this task has not been cancelled by another // 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 // 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 // to this task and our "exit early" flag is not set then try and fetch the bitmap from
@@ -255,6 +277,10 @@ public abstract class ImageWorker {
mImageCache.addBitmapToCache(dataString, bitmap); mImageCache.addBitmapToCache(dataString, bitmap);
} }
if (BuildConfig.DEBUG) {
Log.d(TAG, "doInBackground - finished work");
}
return bitmap; return bitmap;
} }
@@ -270,10 +296,21 @@ public abstract class ImageWorker {
final ImageView imageView = getAttachedImageView(); final ImageView imageView = getAttachedImageView();
if (bitmap != null && imageView != null) { if (bitmap != null && imageView != null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "onPostExecute - setting bitmap");
}
setImageBitmap(imageView, bitmap); setImageBitmap(imageView, bitmap);
} }
} }
@Override
protected void onCancelled(Bitmap bitmap) {
super.onCancelled(bitmap);
synchronized (mPauseWorkLock) {
mPauseWorkLock.notifyAll();
}
}
/** /**
* Returns the ImageView associated with this task as long as the ImageView's task still * Returns the ImageView associated with this task as long as the ImageView's task still
* points to this task as well. Returns null otherwise. * points to this task as well. Returns null otherwise.
@@ -301,7 +338,6 @@ public abstract class ImageWorker {
public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap); super(res, bitmap);
bitmapWorkerTaskReference = bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask); new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
} }
@@ -323,11 +359,11 @@ public abstract class ImageWorker {
final TransitionDrawable td = final TransitionDrawable td =
new TransitionDrawable(new Drawable[] { new TransitionDrawable(new Drawable[] {
new ColorDrawable(android.R.color.transparent), new ColorDrawable(android.R.color.transparent),
new BitmapDrawable(mContext.getResources(), bitmap) new BitmapDrawable(mResources, bitmap)
}); });
// Set background to loading bitmap // Set background to loading bitmap
imageView.setBackgroundDrawable( imageView.setBackgroundDrawable(
new BitmapDrawable(mContext.getResources(), mLoadingBitmap)); new BitmapDrawable(mResources, mLoadingBitmap));
imageView.setImageDrawable(td); imageView.setImageDrawable(td);
td.startTransition(FADE_IN_TIME); td.startTransition(FADE_IN_TIME);
@@ -336,29 +372,71 @@ public abstract class ImageWorker {
} }
} }
/** public void setPauseWork(boolean pauseWork) {
* Set the simple adapter which holds the backing data. synchronized (mPauseWorkLock) {
* mPauseWork = pauseWork;
* @param adapter if (!mPauseWork) {
*/ mPauseWorkLock.notifyAll();
public void setAdapter(ImageWorkerAdapter adapter) { }
mImageWorkerAdapter = adapter; }
} }
/** protected class CacheAsyncTask extends AsyncTask<Object, Void, Void> {
* Get the current adapter.
* @Override
* @return protected Void doInBackground(Object... params) {
*/ switch ((Integer)params[0]) {
public ImageWorkerAdapter getAdapter() { case MESSAGE_CLEAR:
return mImageWorkerAdapter; clearCacheInternal();
break;
case MESSAGE_INIT_DISK_CACHE:
initDiskCacheInternal();
break;
case MESSAGE_FLUSH:
flushCacheInternal();
break;
case MESSAGE_CLOSE:
closeCacheInternal();
break;
}
return null;
}
} }
/** protected void initDiskCacheInternal() {
* A very simple adapter for use with ImageWorker class and subclasses. if (mImageCache != null) {
*/ mImageCache.initDiskCache();
public static abstract class ImageWorkerAdapter { }
public abstract Object getItem(int num); }
public abstract int getSize();
protected void clearCacheInternal() {
if (mImageCache != null) {
mImageCache.clearCache();
}
}
protected void flushCacheInternal() {
if (mImageCache != null) {
mImageCache.flush();
}
}
protected void closeCacheInternal() {
if (mImageCache != null) {
mImageCache.close();
mImageCache = null;
}
}
public void clearCache() {
new CacheAsyncTask().execute(MESSAGE_CLEAR);
}
public void flushCache() {
new CacheAsyncTask().execute(MESSAGE_FLUSH);
}
public void closeCache() {
new CacheAsyncTask().execute(MESSAGE_CLOSE);
} }
} }

View File

@@ -1,83 +0,0 @@
/*
* 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;
}
}

View File

@@ -16,131 +16,61 @@
package com.example.android.bitmapfun.util; package com.example.android.bitmapfun.util;
import android.annotation.SuppressLint; import com.example.android.bitmapfun.ui.ImageDetailActivity;
import android.app.ActivityManager; import com.example.android.bitmapfun.ui.ImageGridActivity;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Environment;
import android.os.StatFs;
import java.io.File; import android.annotation.TargetApi;
import android.os.Build;
import android.os.StrictMode;
/** /**
* Class containing some static utility methods. * Class containing some static utility methods.
*/ */
public class Utils { public class Utils {
public static final int IO_BUFFER_SIZE = 8 * 1024;
private Utils() {}; private Utils() {};
/** @TargetApi(11)
* Workaround for bug pre-Froyo, see here for more info: public static void enableStrictMode() {
* http://android-developers.blogspot.com/2011/09/androids-http-clients.html if (Utils.hasGingerbread()) {
*/ StrictMode.ThreadPolicy.Builder threadPolicyBuilder =
public static void disableConnectionReuseIfNecessary() { new StrictMode.ThreadPolicy.Builder()
// HTTP connection reuse which was buggy pre-froyo .detectAll()
if (hasHttpConnectionBug()) { .penaltyLog();
System.setProperty("http.keepAlive", "false"); StrictMode.VmPolicy.Builder vmPolicyBuilder =
new StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog();
if (Utils.hasHoneycomb()) {
threadPolicyBuilder.penaltyFlashScreen();
vmPolicyBuilder
.setClassInstanceLimit(ImageGridActivity.class, 1)
.setClassInstanceLimit(ImageDetailActivity.class, 1);
}
StrictMode.setThreadPolicy(threadPolicyBuilder.build());
StrictMode.setVmPolicy(vmPolicyBuilder.build());
} }
} }
/** public static boolean hasFroyo() {
* Get the size in bytes of a bitmap. // Can use static final constants like FROYO, declared in later versions
* @param bitmap // of the OS since they are inlined at compile time. This is guaranteed behavior.
* @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; return Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO;
} }
/** public static boolean hasGingerbread() {
* Check if ActionBar is available. return Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD;
* }
* @return
*/ public static boolean hasHoneycomb() {
public static boolean hasActionBar() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
} }
public static boolean hasHoneycombMR1() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1;
}
public static boolean hasJellyBean() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
}
} }