am cbece732: Merge "Sample app for Android U class: Displaying Bitmaps Efficiently" into ics-mr1

* commit 'cbece7322a7c74b74f2fc43cdff9bd9d7ab38632':
  Sample app for Android U class: Displaying Bitmaps Efficiently
This commit is contained in:
Scott Main
2012-04-05 11:31:27 -07:00
committed by Android Git Automerger
30 changed files with 2644 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
<?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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.bitmapfun"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="7"
android:targetSdkVersion="15" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:description="@string/app_description"
android:hardwareAccelerated="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:name=".ui.ImageDetailActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.FullScreen" >
</activity>
<activity
android:name=".ui.ImageGridActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

View File

@@ -0,0 +1,14 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-15

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,36 @@
<?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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/frameLayout"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<ImageView
android:id="@+id/imageView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:contentDescription="@string/imageview_description" />
</FrameLayout>

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.
-->
<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pager"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
</android.support.v4.view.ViewPager>

View File

@@ -0,0 +1,29 @@
<?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.
-->
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/gridView"
style="@style/PhotoGridLayout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:columnWidth="@dimen/image_thumbnail_size"
android:horizontalSpacing="@dimen/image_thumbnail_spacing"
android:numColumns="auto_fit"
android:stretchMode="columnWidth"
android:verticalSpacing="@dimen/image_thumbnail_spacing" >
</GridView>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/clear_cache"
android:icon="@android:drawable/ic_menu_delete"
android:showAsAction="never"
android:title="@string/clear_cache_menu"/>
</menu>

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>
<dimen name="image_thumbnail_size">150dp</dimen>
<dimen name="image_thumbnail_spacing">1dp</dimen>
</resources>

View File

@@ -0,0 +1,36 @@
<?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>
<style name="AppTheme" parent="@android:style/Theme.Holo">
<item name="android:windowActionBarOverlay">true</item>
<item name="android:windowBackground">@android:color/black</item>
<item name="android:actionBarStyle">@style/TranslucentDarkActionBar</item>
</style>
<style name="AppTheme.FullScreen" />
<style name="TranslucentDarkActionBar" parent="@android:style/Widget.Holo.ActionBar">
<item name="android:background">#99000000</item>
</style>
<style name="PhotoGridLayout">
<item name="android:drawSelectorOnTop">true</item>
</style>
</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>
<dimen name="image_thumbnail_size">198dp</dimen>
<dimen name="image_thumbnail_spacing">2dp</dimen>
</resources>

View File

@@ -0,0 +1,24 @@
<?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>
<dimen name="image_thumbnail_size">100dp</dimen>
<dimen name="image_thumbnail_spacing">1dp</dimen>
<dimen name="image_detail_pager_margin">80dp</dimen>
</resources>

View File

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

View File

@@ -0,0 +1,29 @@
<?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>
<style name="AppTheme" parent="android:Theme" />
<style name="AppTheme.FullScreen" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen" />
<style name="PhotoGridLayout">
<item name="android:drawSelectorOnTop">false</item>
<item name="android:listSelector">@null</item>
</style>
</resources>

View File

@@ -0,0 +1,147 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.provider;
import com.example.android.bitmapfun.util.ImageWorker.ImageWorkerAdapter;
/**
* Some simple test data to use for this sample app.
*/
public class Images {
/**
* This are PicasaWeb URLs and could potentially change. Ideally the PicasaWeb API should be
* used to fetch the URLs.
*/
public final static String[] imageUrls = new String[] {
"https://lh6.googleusercontent.com/-jZgveEqb6pg/T3R4kXScycI/AAAAAAAAAE0/xQ7CvpfXDzc/s1024/sample_image_01.jpg",
"https://lh4.googleusercontent.com/-K2FMuOozxU0/T3R4lRAiBTI/AAAAAAAAAE8/a3Eh9JvnnzI/s1024/sample_image_02.jpg",
"https://lh5.googleusercontent.com/-SCS5C646rxM/T3R4l7QB6xI/AAAAAAAAAFE/xLcuVv3CUyA/s1024/sample_image_03.jpg",
"https://lh6.googleusercontent.com/-f0NJR6-_Thg/T3R4mNex2wI/AAAAAAAAAFI/45oug4VE8MI/s1024/sample_image_04.jpg",
"https://lh3.googleusercontent.com/-n-xcJmiI0pg/T3R4mkSchHI/AAAAAAAAAFU/EoiNNb7kk3A/s1024/sample_image_05.jpg",
"https://lh3.googleusercontent.com/-X43vAudm7f4/T3R4nGSChJI/AAAAAAAAAFk/3bna6D-2EE8/s1024/sample_image_06.jpg",
"https://lh5.googleusercontent.com/-MpZneqIyjXU/T3R4nuGO1aI/AAAAAAAAAFg/r09OPjLx1ZY/s1024/sample_image_07.jpg",
"https://lh6.googleusercontent.com/-ql3YNfdClJo/T3XvW9apmFI/AAAAAAAAAL4/_6HFDzbahc4/s1024/sample_image_08.jpg",
"https://lh5.googleusercontent.com/-Pxa7eqF4cyc/T3R4oasvPEI/AAAAAAAAAF0/-uYDH92h8LA/s1024/sample_image_09.jpg",
"https://lh4.googleusercontent.com/-Li-rjhFEuaI/T3R4o-VUl4I/AAAAAAAAAF8/5E5XdMnP1oE/s1024/sample_image_10.jpg",
"https://lh5.googleusercontent.com/-_HU4fImgFhA/T3R4pPVIwWI/AAAAAAAAAGA/0RfK_Vkgth4/s1024/sample_image_11.jpg",
"https://lh6.googleusercontent.com/-0gnNrVjwa0Y/T3R4peGYJwI/AAAAAAAAAGU/uX_9wvRPM9I/s1024/sample_image_12.jpg",
"https://lh3.googleusercontent.com/-HBxuzALS_Zs/T3R4qERykaI/AAAAAAAAAGQ/_qQ16FaZ1q0/s1024/sample_image_13.jpg",
"https://lh4.googleusercontent.com/-cKojDrARNjQ/T3R4qfWSGPI/AAAAAAAAAGY/MR5dnbNaPyY/s1024/sample_image_14.jpg",
"https://lh3.googleusercontent.com/-WujkdYfcyZ8/T3R4qrIMGUI/AAAAAAAAAGk/277LIdgvnjg/s1024/sample_image_15.jpg",
"https://lh6.googleusercontent.com/-FMHR7Vy3PgI/T3R4rOXlEKI/AAAAAAAAAGs/VeXrDNDBkaw/s1024/sample_image_16.jpg",
"https://lh4.googleusercontent.com/-mrR0AJyNTH0/T3R4rZs6CuI/AAAAAAAAAG0/UE1wQqCOqLA/s1024/sample_image_17.jpg",
"https://lh6.googleusercontent.com/-z77w0eh3cow/T3R4rnLn05I/AAAAAAAAAG4/BaerfWoNucU/s1024/sample_image_18.jpg",
"https://lh5.googleusercontent.com/-aWVwh1OU5Bk/T3R4sAWw0yI/AAAAAAAAAHE/4_KAvJttFwA/s1024/sample_image_19.jpg",
"https://lh6.googleusercontent.com/-q-js52DMnWQ/T3R4tZhY2sI/AAAAAAAAAHM/A8kjp2Ivdqg/s1024/sample_image_20.jpg",
"https://lh5.googleusercontent.com/-_jIzvvzXKn4/T3R4t7xpdVI/AAAAAAAAAHU/7QC6eZ10jgs/s1024/sample_image_21.jpg",
"https://lh3.googleusercontent.com/-lnGi4IMLpwU/T3R4uCMa7vI/AAAAAAAAAHc/1zgzzz6qTpk/s1024/sample_image_22.jpg",
"https://lh5.googleusercontent.com/-fFCzKjFPsPc/T3R4u0SZPFI/AAAAAAAAAHk/sbgjzrktOK0/s1024/sample_image_23.jpg",
"https://lh4.googleusercontent.com/-8TqoW5gBE_Y/T3R4vBS3NPI/AAAAAAAAAHs/EZYvpNsaNXk/s1024/sample_image_24.jpg",
"https://lh6.googleusercontent.com/-gc4eQ3ySdzs/T3R4vafoA7I/AAAAAAAAAH4/yKii5P6tqDE/s1024/sample_image_25.jpg",
"https://lh5.googleusercontent.com/--NYOPCylU7Q/T3R4vjAiWkI/AAAAAAAAAH8/IPNx5q3ptRA/s1024/sample_image_26.jpg",
"https://lh6.googleusercontent.com/-9IJM8so4vCI/T3R4vwJO2yI/AAAAAAAAAIE/ljlr-cwuqZM/s1024/sample_image_27.jpg",
"https://lh4.googleusercontent.com/-KW6QwOHfhBs/T3R4w0RsQiI/AAAAAAAAAIM/uEFLVgHPFCk/s1024/sample_image_28.jpg",
"https://lh4.googleusercontent.com/-z2557Ec1ctY/T3R4x3QA2hI/AAAAAAAAAIk/9-GzPL1lTWE/s1024/sample_image_29.jpg",
"https://lh5.googleusercontent.com/-LaKXAn4Kr1c/T3R4yc5b4lI/AAAAAAAAAIY/fMgcOVQfmD0/s1024/sample_image_30.jpg",
"https://lh4.googleusercontent.com/-F9LRToJoQdo/T3R4yrLtyQI/AAAAAAAAAIg/ri9uUCWuRmo/s1024/sample_image_31.jpg",
"https://lh4.googleusercontent.com/-6X-xBwP-QpI/T3R4zGVboII/AAAAAAAAAIs/zYH4PjjngY0/s1024/sample_image_32.jpg",
"https://lh5.googleusercontent.com/-VdLRjbW4LAs/T3R4zXu3gUI/AAAAAAAAAIw/9aFp9t7mCPg/s1024/sample_image_33.jpg",
"https://lh6.googleusercontent.com/-gL6R17_fDJU/T3R4zpIXGjI/AAAAAAAAAI8/Q2Vjx-L9X20/s1024/sample_image_34.jpg",
"https://lh3.googleusercontent.com/-1fGH4YJXEzo/T3R40Y1B7KI/AAAAAAAAAJE/MnTsa77g-nk/s1024/sample_image_35.jpg",
"https://lh4.googleusercontent.com/-Ql0jHSrea-A/T3R403mUfFI/AAAAAAAAAJM/qzI4SkcH9tY/s1024/sample_image_36.jpg",
"https://lh5.googleusercontent.com/-BL5FIBR_tzI/T3R41DA0AKI/AAAAAAAAAJk/GZfeeb-SLM0/s1024/sample_image_37.jpg",
"https://lh4.googleusercontent.com/-wF2Vc9YDutw/T3R41fR2BCI/AAAAAAAAAJc/JdU1sHdMRAk/s1024/sample_image_38.jpg",
"https://lh6.googleusercontent.com/-ZWHiPehwjTI/T3R41zuaKCI/AAAAAAAAAJg/hR3QJ1v3REg/s1024/sample_image_39.jpg",
};
/**
* This are PicasaWeb thumbnail URLs and could potentially change. Ideally the PicasaWeb API
* should be used to fetch the URLs.
*/
public final static String[] imageThumbUrls = new String[] {
"https://lh6.googleusercontent.com/-jZgveEqb6pg/T3R4kXScycI/AAAAAAAAAE0/xQ7CvpfXDzc/s160-c/sample_image_01.jpg",
"https://lh4.googleusercontent.com/-K2FMuOozxU0/T3R4lRAiBTI/AAAAAAAAAE8/a3Eh9JvnnzI/s160-c/sample_image_02.jpg",
"https://lh5.googleusercontent.com/-SCS5C646rxM/T3R4l7QB6xI/AAAAAAAAAFE/xLcuVv3CUyA/s160-c/sample_image_03.jpg",
"https://lh6.googleusercontent.com/-f0NJR6-_Thg/T3R4mNex2wI/AAAAAAAAAFI/45oug4VE8MI/s160-c/sample_image_04.jpg",
"https://lh3.googleusercontent.com/-n-xcJmiI0pg/T3R4mkSchHI/AAAAAAAAAFU/EoiNNb7kk3A/s160-c/sample_image_05.jpg",
"https://lh3.googleusercontent.com/-X43vAudm7f4/T3R4nGSChJI/AAAAAAAAAFk/3bna6D-2EE8/s160-c/sample_image_06.jpg",
"https://lh5.googleusercontent.com/-MpZneqIyjXU/T3R4nuGO1aI/AAAAAAAAAFg/r09OPjLx1ZY/s160-c/sample_image_07.jpg",
"https://lh6.googleusercontent.com/-ql3YNfdClJo/T3XvW9apmFI/AAAAAAAAAL4/_6HFDzbahc4/s160-c/sample_image_08.jpg",
"https://lh5.googleusercontent.com/-Pxa7eqF4cyc/T3R4oasvPEI/AAAAAAAAAF0/-uYDH92h8LA/s160-c/sample_image_09.jpg",
"https://lh4.googleusercontent.com/-Li-rjhFEuaI/T3R4o-VUl4I/AAAAAAAAAF8/5E5XdMnP1oE/s160-c/sample_image_10.jpg",
"https://lh5.googleusercontent.com/-_HU4fImgFhA/T3R4pPVIwWI/AAAAAAAAAGA/0RfK_Vkgth4/s160-c/sample_image_11.jpg",
"https://lh6.googleusercontent.com/-0gnNrVjwa0Y/T3R4peGYJwI/AAAAAAAAAGU/uX_9wvRPM9I/s160-c/sample_image_12.jpg",
"https://lh3.googleusercontent.com/-HBxuzALS_Zs/T3R4qERykaI/AAAAAAAAAGQ/_qQ16FaZ1q0/s160-c/sample_image_13.jpg",
"https://lh4.googleusercontent.com/-cKojDrARNjQ/T3R4qfWSGPI/AAAAAAAAAGY/MR5dnbNaPyY/s160-c/sample_image_14.jpg",
"https://lh3.googleusercontent.com/-WujkdYfcyZ8/T3R4qrIMGUI/AAAAAAAAAGk/277LIdgvnjg/s160-c/sample_image_15.jpg",
"https://lh6.googleusercontent.com/-FMHR7Vy3PgI/T3R4rOXlEKI/AAAAAAAAAGs/VeXrDNDBkaw/s160-c/sample_image_16.jpg",
"https://lh4.googleusercontent.com/-mrR0AJyNTH0/T3R4rZs6CuI/AAAAAAAAAG0/UE1wQqCOqLA/s160-c/sample_image_17.jpg",
"https://lh6.googleusercontent.com/-z77w0eh3cow/T3R4rnLn05I/AAAAAAAAAG4/BaerfWoNucU/s160-c/sample_image_18.jpg",
"https://lh5.googleusercontent.com/-aWVwh1OU5Bk/T3R4sAWw0yI/AAAAAAAAAHE/4_KAvJttFwA/s160-c/sample_image_19.jpg",
"https://lh6.googleusercontent.com/-q-js52DMnWQ/T3R4tZhY2sI/AAAAAAAAAHM/A8kjp2Ivdqg/s160-c/sample_image_20.jpg",
"https://lh5.googleusercontent.com/-_jIzvvzXKn4/T3R4t7xpdVI/AAAAAAAAAHU/7QC6eZ10jgs/s160-c/sample_image_21.jpg",
"https://lh3.googleusercontent.com/-lnGi4IMLpwU/T3R4uCMa7vI/AAAAAAAAAHc/1zgzzz6qTpk/s160-c/sample_image_22.jpg",
"https://lh5.googleusercontent.com/-fFCzKjFPsPc/T3R4u0SZPFI/AAAAAAAAAHk/sbgjzrktOK0/s160-c/sample_image_23.jpg",
"https://lh4.googleusercontent.com/-8TqoW5gBE_Y/T3R4vBS3NPI/AAAAAAAAAHs/EZYvpNsaNXk/s160-c/sample_image_24.jpg",
"https://lh6.googleusercontent.com/-gc4eQ3ySdzs/T3R4vafoA7I/AAAAAAAAAH4/yKii5P6tqDE/s160-c/sample_image_25.jpg",
"https://lh5.googleusercontent.com/--NYOPCylU7Q/T3R4vjAiWkI/AAAAAAAAAH8/IPNx5q3ptRA/s160-c/sample_image_26.jpg",
"https://lh6.googleusercontent.com/-9IJM8so4vCI/T3R4vwJO2yI/AAAAAAAAAIE/ljlr-cwuqZM/s160-c/sample_image_27.jpg",
"https://lh4.googleusercontent.com/-KW6QwOHfhBs/T3R4w0RsQiI/AAAAAAAAAIM/uEFLVgHPFCk/s160-c/sample_image_28.jpg",
"https://lh4.googleusercontent.com/-z2557Ec1ctY/T3R4x3QA2hI/AAAAAAAAAIk/9-GzPL1lTWE/s160-c/sample_image_29.jpg",
"https://lh5.googleusercontent.com/-LaKXAn4Kr1c/T3R4yc5b4lI/AAAAAAAAAIY/fMgcOVQfmD0/s160-c/sample_image_30.jpg",
"https://lh4.googleusercontent.com/-F9LRToJoQdo/T3R4yrLtyQI/AAAAAAAAAIg/ri9uUCWuRmo/s160-c/sample_image_31.jpg",
"https://lh4.googleusercontent.com/-6X-xBwP-QpI/T3R4zGVboII/AAAAAAAAAIs/zYH4PjjngY0/s160-c/sample_image_32.jpg",
"https://lh5.googleusercontent.com/-VdLRjbW4LAs/T3R4zXu3gUI/AAAAAAAAAIw/9aFp9t7mCPg/s160-c/sample_image_33.jpg",
"https://lh6.googleusercontent.com/-gL6R17_fDJU/T3R4zpIXGjI/AAAAAAAAAI8/Q2Vjx-L9X20/s160-c/sample_image_34.jpg",
"https://lh3.googleusercontent.com/-1fGH4YJXEzo/T3R40Y1B7KI/AAAAAAAAAJE/MnTsa77g-nk/s160-c/sample_image_35.jpg",
"https://lh4.googleusercontent.com/-Ql0jHSrea-A/T3R403mUfFI/AAAAAAAAAJM/qzI4SkcH9tY/s160-c/sample_image_36.jpg",
"https://lh5.googleusercontent.com/-BL5FIBR_tzI/T3R41DA0AKI/AAAAAAAAAJk/GZfeeb-SLM0/s160-c/sample_image_37.jpg",
"https://lh4.googleusercontent.com/-wF2Vc9YDutw/T3R41fR2BCI/AAAAAAAAAJc/JdU1sHdMRAk/s160-c/sample_image_38.jpg",
"https://lh6.googleusercontent.com/-ZWHiPehwjTI/T3R41zuaKCI/AAAAAAAAAJg/hR3QJ1v3REg/s160-c/sample_image_39.jpg",
};
/**
* Simple static adapter to use for images.
*/
public final static ImageWorkerAdapter imageWorkerUrlsAdapter = new ImageWorkerAdapter() {
@Override
public Object getItem(int num) {
return Images.imageUrls[num];
}
@Override
public int getSize() {
return Images.imageUrls.length;
}
};
/**
* Simple static adapter to use for image thumbnails.
*/
public final static ImageWorkerAdapter imageThumbWorkerUrlsAdapter = new ImageWorkerAdapter() {
@Override
public Object getItem(int num) {
return Images.imageThumbUrls[num];
}
@Override
public int getSize() {
return Images.imageThumbUrls.length;
}
};
}

View File

@@ -0,0 +1,203 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.ui;
import android.annotation.SuppressLint;
import android.app.ActionBar;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;
import android.util.DisplayMetrics;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.WindowManager.LayoutParams;
import android.widget.Toast;
import com.example.android.bitmapfun.R;
import com.example.android.bitmapfun.provider.Images;
import com.example.android.bitmapfun.util.DiskLruCache;
import com.example.android.bitmapfun.util.ImageCache;
import com.example.android.bitmapfun.util.ImageFetcher;
import com.example.android.bitmapfun.util.ImageResizer;
import com.example.android.bitmapfun.util.ImageWorker;
import com.example.android.bitmapfun.util.Utils;
public class ImageDetailActivity extends FragmentActivity implements OnClickListener {
private static final String IMAGE_CACHE_DIR = "images";
public static final String EXTRA_IMAGE = "extra_image";
private ImagePagerAdapter mAdapter;
private ImageResizer mImageWorker;
private ViewPager mPager;
@SuppressLint("NewApi")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.image_detail_pager);
// Fetch screen height and width, to use as our max size when loading images as this
// activity runs full screen
final DisplayMetrics displaymetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
final int height = displaymetrics.heightPixels;
final int width = displaymetrics.widthPixels;
final int longest = height > width ? height : width;
// The ImageWorker takes care of loading images into our ImageView children asynchronously
mImageWorker = new ImageFetcher(this, longest);
mImageWorker.setAdapter(Images.imageWorkerUrlsAdapter);
mImageWorker.setImageCache(ImageCache.findOrCreateCache(this, IMAGE_CACHE_DIR));
mImageWorker.setImageFadeIn(false);
// Set up ViewPager and backing adapter
mAdapter = new ImagePagerAdapter(getSupportFragmentManager(),
mImageWorker.getAdapter().getSize());
mPager = (ViewPager) findViewById(R.id.pager);
mPager.setAdapter(mAdapter);
mPager.setPageMargin((int) getResources().getDimension(R.dimen.image_detail_pager_margin));
// Set up activity to go full screen
getWindow().addFlags(LayoutParams.FLAG_FULLSCREEN);
// Enable some additional newer visibility and ActionBar features to create a more immersive
// photo viewing experience
if (Utils.hasActionBar()) {
final ActionBar actionBar = getActionBar();
// Enable "up" navigation on ActionBar icon and hide title text
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowTitleEnabled(false);
// Start low profile mode and hide ActionBar
mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
actionBar.hide();
// Hide and show the ActionBar as the visibility changes
mPager.setOnSystemUiVisibilityChangeListener(
new View.OnSystemUiVisibilityChangeListener() {
@Override
public void onSystemUiVisibilityChange(int vis) {
if ((vis & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0) {
actionBar.hide();
} else {
actionBar.show();
}
}
});
}
// Set the current item based on the extra passed in to this activity
final int extraCurrentItem = getIntent().getIntExtra(EXTRA_IMAGE, -1);
if (extraCurrentItem != -1) {
mPager.setCurrentItem(extraCurrentItem);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// Home or "up" navigation
final Intent intent = new Intent(this, ImageGridActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true;
case R.id.clear_cache:
final ImageCache cache = mImageWorker.getImageCache();
if (cache != null) {
mImageWorker.getImageCache().clearCaches();
DiskLruCache.clearCache(this, ImageFetcher.HTTP_CACHE_DIR);
Toast.makeText(this, R.string.clear_cache_complete,
Toast.LENGTH_SHORT).show();
}
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main_menu, menu);
return true;
}
/**
* Called by the ViewPager child fragments to load images via the one ImageWorker
*
* @return
*/
public ImageWorker getImageWorker() {
return mImageWorker;
}
/**
* The main adapter that backs the ViewPager. A subclass of FragmentStatePagerAdapter as there
* could be a large number of items in the ViewPager and we don't want to retain them all in
* memory at once but create/destroy them on the fly.
*/
private class ImagePagerAdapter extends FragmentStatePagerAdapter {
private final int mSize;
public ImagePagerAdapter(FragmentManager fm, int size) {
super(fm);
mSize = size;
}
@Override
public int getCount() {
return mSize;
}
@Override
public Fragment getItem(int position) {
return ImageDetailFragment.newInstance(position);
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
final ImageDetailFragment fragment = (ImageDetailFragment) object;
// As the item gets destroyed we try and cancel any existing work.
fragment.cancelWork();
super.destroyItem(container, position, object);
}
}
/**
* Set on the ImageView in the ViewPager children fragments, to enable/disable low profile mode
* when the ImageView is touched.
*/
@SuppressLint("NewApi")
@Override
public void onClick(View v) {
final int vis = mPager.getSystemUiVisibility();
if ((vis & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0) {
mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
} else {
mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
}
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.ui;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.example.android.bitmapfun.R;
import com.example.android.bitmapfun.util.ImageWorker;
import com.example.android.bitmapfun.util.Utils;
/**
* This fragment will populate the children of the ViewPager from {@link ImageDetailActivity}.
*/
public class ImageDetailFragment extends Fragment {
private static final String IMAGE_DATA_EXTRA = "resId";
private int mImageNum;
private ImageView mImageView;
private ImageWorker mImageWorker;
/**
* Factory method to generate a new instance of the fragment given an image number.
*
* @param imageNum The image number within the parent adapter to load
* @return A new instance of ImageDetailFragment with imageNum extras
*/
public static ImageDetailFragment newInstance(int imageNum) {
final ImageDetailFragment f = new ImageDetailFragment();
final Bundle args = new Bundle();
args.putInt(IMAGE_DATA_EXTRA, imageNum);
f.setArguments(args);
return f;
}
/**
* Empty constructor as per the Fragment documentation
*/
public ImageDetailFragment() {}
/**
* Populate image number from extra, use the convenience factory method
* {@link ImageDetailFragment#newInstance(int)} to create this fragment.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate and locate the main ImageView
final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
mImageView = (ImageView) v.findViewById(R.id.imageView);
return v;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Use the parent activity to load the image asynchronously into the ImageView (so a single
// cache can be used over all pages in the ViewPager
if (ImageDetailActivity.class.isInstance(getActivity())) {
mImageWorker = ((ImageDetailActivity) getActivity()).getImageWorker();
mImageWorker.loadImage(mImageNum, mImageView);
}
// Pass clicks on the ImageView to the parent activity to handle
if (OnClickListener.class.isInstance(getActivity()) && Utils.hasActionBar()) {
mImageView.setOnClickListener((OnClickListener) getActivity());
}
}
/**
* Cancels the asynchronous work taking place on the ImageView, called by the adapter backing
* the ViewPager when the child is destroyed.
*/
public void cancelWork() {
ImageWorker.cancelWork(mImageView);
mImageView.setImageDrawable(null);
mImageView = null;
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.ui;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTransaction;
/**
* Simple FragmentActivity to hold the main {@link ImageGridFragment} and not much else.
*/
public class ImageGridActivity extends FragmentActivity {
private static final String TAG = "ImageGridFragment";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getSupportFragmentManager().findFragmentByTag(TAG) == null) {
final FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(android.R.id.content, new ImageGridFragment(), TAG);
ft.commit();
}
}
}

View File

@@ -0,0 +1,300 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.ui;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.Toast;
import com.example.android.bitmapfun.BuildConfig;
import com.example.android.bitmapfun.R;
import com.example.android.bitmapfun.provider.Images;
import com.example.android.bitmapfun.util.DiskLruCache;
import com.example.android.bitmapfun.util.ImageCache;
import com.example.android.bitmapfun.util.ImageCache.ImageCacheParams;
import com.example.android.bitmapfun.util.ImageFetcher;
import com.example.android.bitmapfun.util.ImageResizer;
import com.example.android.bitmapfun.util.Utils;
/**
* The main fragment that powers the ImageGridActivity screen. Fairly straight forward GridView
* implementation with the key addition being the ImageWorker class w/ImageCache to load children
* asynchronously, keeping the UI nice and smooth and caching thumbnails for quick retrieval. The
* cache is retained over configuration changes like orientation change so the images are populated
* quickly as the user rotates the device.
*/
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
private static final String TAG = "ImageGridFragment";
private static final String IMAGE_CACHE_DIR = "thumbs";
private int mImageThumbSize;
private int mImageThumbSpacing;
private ImageAdapter mAdapter;
private ImageResizer mImageWorker;
/**
* Empty constructor as per the Fragment documentation
*/
public ImageGridFragment() {}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
mImageThumbSize = getResources().getDimensionPixelSize(R.dimen.image_thumbnail_size);
mImageThumbSpacing = getResources().getDimensionPixelSize(R.dimen.image_thumbnail_spacing);
mAdapter = new ImageAdapter(getActivity());
ImageCacheParams cacheParams = new ImageCacheParams(IMAGE_CACHE_DIR);
// Allocate a third of the per-app memory limit to the bitmap memory cache. This value
// should be chosen carefully based on a number of factors. Refer to the corresponding
// Android Training class for more discussion:
// http://developer.android.com/training/displaying-bitmaps/
// In this case, we aren't using memory for much else other than this activity and the
// ImageDetailActivity so a third lets us keep all our sample image thumbnails in memory
// at once.
cacheParams.memCacheSize = 1024 * 1024 * Utils.getMemoryClass(getActivity()) / 3;
// The ImageWorker takes care of loading images into our ImageView children asynchronously
mImageWorker = new ImageFetcher(getActivity(), mImageThumbSize);
mImageWorker.setAdapter(Images.imageThumbWorkerUrlsAdapter);
mImageWorker.setLoadingImage(R.drawable.empty_photo);
mImageWorker.setImageCache(ImageCache.findOrCreateCache(getActivity(), cacheParams));
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
mGridView.setAdapter(mAdapter);
mGridView.setOnItemClickListener(this);
// This listener is used to get the final width of the GridView and then calculate the
// number of columns and the width of each column. The width of each column is variable
// as the GridView has stretchMode=columnWidth. The column width is used to set the height
// of each view so we get nice square thumbnails.
mGridView.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (mAdapter.getNumColumns() == 0) {
final int numColumns = (int) Math.floor(
mGridView.getWidth() / (mImageThumbSize + mImageThumbSpacing));
if (numColumns > 0) {
final int columnWidth =
(mGridView.getWidth() / numColumns) - mImageThumbSpacing;
mAdapter.setNumColumns(numColumns);
mAdapter.setItemHeight(columnWidth);
if (BuildConfig.DEBUG) {
Log.d(TAG, "onCreateView - numColumns set to " + numColumns);
}
}
}
}
});
return v;
}
@Override
public void onResume() {
super.onResume();
mImageWorker.setExitTasksEarly(false);
mAdapter.notifyDataSetChanged();
}
@Override
public void onPause() {
super.onPause();
mImageWorker.setExitTasksEarly(true);
}
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
i.putExtra(ImageDetailActivity.EXTRA_IMAGE, (int) id);
startActivity(i);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.main_menu, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.clear_cache:
final ImageCache cache = mImageWorker.getImageCache();
if (cache != null) {
mImageWorker.getImageCache().clearCaches();
DiskLruCache.clearCache(getActivity(), ImageFetcher.HTTP_CACHE_DIR);
Toast.makeText(getActivity(), R.string.clear_cache_complete,
Toast.LENGTH_SHORT).show();
}
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* The main adapter that backs the GridView. This is fairly standard except the number of
* columns in the GridView is used to create a fake top row of empty views as we use a
* transparent ActionBar and don't want the real top row of images to start off covered by it.
*/
private class ImageAdapter extends BaseAdapter {
private final Context mContext;
private int mItemHeight = 0;
private int mNumColumns = 0;
private int mActionBarHeight = -1;
private GridView.LayoutParams mImageViewLayoutParams;
public ImageAdapter(Context context) {
super();
mContext = context;
mImageViewLayoutParams = new GridView.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
@Override
public int getCount() {
// Size of adapter + number of columns for top empty row
return mImageWorker.getAdapter().getSize() + mNumColumns;
}
@Override
public Object getItem(int position) {
return position < mNumColumns ?
null : mImageWorker.getAdapter().getItem(position - mNumColumns);
}
@Override
public long getItemId(int position) {
return position < mNumColumns ? 0 : position - mNumColumns;
}
@Override
public int getViewTypeCount() {
// Two types of views, the normal ImageView and the top row of empty views
return 2;
}
@Override
public int getItemViewType(int position) {
return (position < mNumColumns) ? 1 : 0;
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public View getView(int position, View convertView, ViewGroup container) {
// First check if this is the top row
if (position < mNumColumns) {
if (convertView == null) {
convertView = new View(mContext);
}
// Calculate ActionBar height
if (mActionBarHeight < 0) {
TypedValue tv = new TypedValue();
if (mContext.getTheme().resolveAttribute(
android.R.attr.actionBarSize, tv, true)) {
mActionBarHeight = TypedValue.complexToDimensionPixelSize(
tv.data, mContext.getResources().getDisplayMetrics());
} else {
// No ActionBar style (pre-Honeycomb or ActionBar not in theme)
mActionBarHeight = 0;
}
}
// Set empty view with height of ActionBar
convertView.setLayoutParams(new AbsListView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, mActionBarHeight));
return convertView;
}
// Now handle the main ImageView thumbnails
ImageView imageView;
if (convertView == null) { // if it's not recycled, instantiate and initialize
imageView = new ImageView(mContext);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageView.setLayoutParams(mImageViewLayoutParams);
} else { // Otherwise re-use the converted view
imageView = (ImageView) convertView;
}
// Check the height matches our calculated column width
if (imageView.getLayoutParams().height != mItemHeight) {
imageView.setLayoutParams(mImageViewLayoutParams);
}
// Finally load the image asynchronously into the ImageView, this also takes care of
// setting a placeholder image while the background thread runs
mImageWorker.loadImage(position - mNumColumns, imageView);
return imageView;
}
/**
* Sets the item height. Useful for when we know the column width so the height can be set
* to match.
*
* @param height
*/
public void setItemHeight(int height) {
if (height == mItemHeight) {
return;
}
mItemHeight = height;
mImageViewLayoutParams =
new GridView.LayoutParams(LayoutParams.MATCH_PARENT, mItemHeight);
mImageWorker.setImageSize(height);
notifyDataSetChanged();
}
public void setNumColumns(int numColumns) {
mNumColumns = numColumns;
}
public int getNumColumns() {
return mNumColumns;
}
}
}

View File

@@ -0,0 +1,336 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.util.Log;
import com.example.android.bitmapfun.BuildConfig;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
/**
* A simple disk LRU bitmap cache to illustrate how a disk cache would be used for bitmap caching. A
* much more robust and efficient disk LRU cache solution can be found in the ICS source code
* (libcore/luni/src/main/java/libcore/io/DiskLruCache.java) and is preferable to this simple
* implementation.
*/
public class DiskLruCache {
private static final String TAG = "DiskLruCache";
private static final String CACHE_FILENAME_PREFIX = "cache_";
private static final int MAX_REMOVALS = 4;
private static final int INITIAL_CAPACITY = 32;
private static final float LOAD_FACTOR = 0.75f;
private final File mCacheDir;
private int cacheSize = 0;
private int cacheByteSize = 0;
private final int maxCacheItemSize = 64; // 64 item default
private long maxCacheByteSize = 1024 * 1024 * 5; // 5MB default
private CompressFormat mCompressFormat = CompressFormat.JPEG;
private int mCompressQuality = 70;
private final Map<String, String> mLinkedHashMap =
Collections.synchronizedMap(new LinkedHashMap<String, String>(
INITIAL_CAPACITY, LOAD_FACTOR, true));
/**
* A filename filter to use to identify the cache filenames which have CACHE_FILENAME_PREFIX
* prepended.
*/
private static final FilenameFilter cacheFileFilter = new FilenameFilter() {
@Override
public boolean accept(File dir, String filename) {
return filename.startsWith(CACHE_FILENAME_PREFIX);
}
};
/**
* Used to fetch an instance of DiskLruCache.
*
* @param context
* @param cacheDir
* @param maxByteSize
* @return
*/
public static DiskLruCache openCache(Context context, File cacheDir, long maxByteSize) {
if (!cacheDir.exists()) {
cacheDir.mkdir();
}
if (cacheDir.isDirectory() && cacheDir.canWrite()
&& Utils.getUsableSpace(cacheDir) > maxByteSize) {
return new DiskLruCache(cacheDir, maxByteSize);
}
return null;
}
/**
* Constructor that should not be called directly, instead use
* {@link DiskLruCache#openCache(Context, File, long)} which runs some extra checks before
* creating a DiskLruCache instance.
*
* @param cacheDir
* @param maxByteSize
*/
private DiskLruCache(File cacheDir, long maxByteSize) {
mCacheDir = cacheDir;
maxCacheByteSize = maxByteSize;
}
/**
* Add a bitmap to the disk cache.
*
* @param key A unique identifier for the bitmap.
* @param data The bitmap to store.
*/
public void put(String key, Bitmap data) {
synchronized (mLinkedHashMap) {
if (mLinkedHashMap.get(key) == null) {
try {
final String file = createFilePath(mCacheDir, key);
if (writeBitmapToFile(data, file)) {
put(key, file);
flushCache();
}
} catch (final FileNotFoundException e) {
Log.e(TAG, "Error in put: " + e.getMessage());
} catch (final IOException e) {
Log.e(TAG, "Error in put: " + e.getMessage());
}
}
}
}
private void put(String key, String file) {
mLinkedHashMap.put(key, file);
cacheSize = mLinkedHashMap.size();
cacheByteSize += new File(file).length();
}
/**
* Flush the cache, removing oldest entries if the total size is over the specified cache size.
* Note that this isn't keeping track of stale files in the cache directory that aren't in the
* HashMap. If the images and keys in the disk cache change often then they probably won't ever
* be removed.
*/
private void flushCache() {
Entry<String, String> eldestEntry;
File eldestFile;
long eldestFileSize;
int count = 0;
while (count < MAX_REMOVALS &&
(cacheSize > maxCacheItemSize || cacheByteSize > maxCacheByteSize)) {
eldestEntry = mLinkedHashMap.entrySet().iterator().next();
eldestFile = new File(eldestEntry.getValue());
eldestFileSize = eldestFile.length();
mLinkedHashMap.remove(eldestEntry.getKey());
eldestFile.delete();
cacheSize = mLinkedHashMap.size();
cacheByteSize -= eldestFileSize;
count++;
if (BuildConfig.DEBUG) {
Log.d(TAG, "flushCache - Removed cache file, " + eldestFile + ", "
+ eldestFileSize);
}
}
}
/**
* Get an image from the disk cache.
*
* @param key The unique identifier for the bitmap
* @return The bitmap or null if not found
*/
public Bitmap get(String key) {
synchronized (mLinkedHashMap) {
final String file = mLinkedHashMap.get(key);
if (file != null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache hit");
}
return BitmapFactory.decodeFile(file);
} else {
final String existingFile = createFilePath(mCacheDir, key);
if (new File(existingFile).exists()) {
put(key, existingFile);
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache hit (existing file)");
}
return BitmapFactory.decodeFile(existingFile);
}
}
return null;
}
}
/**
* Checks if a specific key exist in the cache.
*
* @param key The unique identifier for the bitmap
* @return true if found, false otherwise
*/
public boolean containsKey(String key) {
// See if the key is in our HashMap
if (mLinkedHashMap.containsKey(key)) {
return true;
}
// Now check if there's an actual file that exists based on the key
final String existingFile = createFilePath(mCacheDir, key);
if (new File(existingFile).exists()) {
// File found, add it to the HashMap for future use
put(key, existingFile);
return true;
}
return false;
}
/**
* Removes all disk cache entries from this instance cache dir
*/
public void clearCache() {
DiskLruCache.clearCache(mCacheDir);
}
/**
* Removes all disk cache entries from the application cache directory in the uniqueName
* sub-directory.
*
* @param context The context to use
* @param uniqueName A unique cache directory name to append to the app cache directory
*/
public static void clearCache(Context context, String uniqueName) {
File cacheDir = getDiskCacheDir(context, uniqueName);
clearCache(cacheDir);
}
/**
* Removes all disk cache entries from the given directory. This should not be called directly,
* call {@link DiskLruCache#clearCache(Context, String)} or {@link DiskLruCache#clearCache()}
* instead.
*
* @param cacheDir The directory to remove the cache files from
*/
private static void clearCache(File cacheDir) {
final File[] files = cacheDir.listFiles(cacheFileFilter);
for (int i=0; i<files.length; i++) {
files[i].delete();
}
}
/**
* 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.getExternalStorageState() == Environment.MEDIA_MOUNTED ||
!Utils.isExternalStorageRemovable() ?
Utils.getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}
/**
* Creates a constant cache file path given a target cache directory and an image key.
*
* @param cacheDir
* @param key
* @return
*/
public static String createFilePath(File cacheDir, String key) {
try {
// Use URLEncoder to ensure we have a valid filename, a tad hacky but it will do for
// this example
return cacheDir.getAbsolutePath() + File.separator +
CACHE_FILENAME_PREFIX + URLEncoder.encode(key.replace("*", ""), "UTF-8");
} catch (final UnsupportedEncodingException e) {
Log.e(TAG, "createFilePath - " + e);
}
return null;
}
/**
* Create a constant cache file path using the current cache directory and an image key.
*
* @param key
* @return
*/
public String createFilePath(String key) {
return createFilePath(mCacheDir, key);
}
/**
* Sets the target compression format and quality for images written to the disk cache.
*
* @param compressFormat
* @param quality
*/
public void setCompressParams(CompressFormat compressFormat, int quality) {
mCompressFormat = compressFormat;
mCompressQuality = quality;
}
/**
* Writes a bitmap to a file. Call {@link DiskLruCache#setCompressParams(CompressFormat, int)}
* first to set the target bitmap compression and format.
*
* @param bitmap
* @param file
* @return
*/
private boolean writeBitmapToFile(Bitmap bitmap, String file)
throws IOException, FileNotFoundException {
OutputStream out = null;
try {
out = new BufferedOutputStream(new FileOutputStream(file), Utils.IO_BUFFER_SIZE);
return bitmap.compress(mCompressFormat, mCompressQuality, out);
} finally {
if (out != null) {
out.close();
}
}
}
}

View File

@@ -0,0 +1,217 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.support.v4.app.FragmentActivity;
import android.support.v4.util.LruCache;
import android.util.Log;
import com.example.android.bitmapfun.BuildConfig;
import java.io.File;
/**
* This class holds our bitmap caches (memory and disk).
*/
public class ImageCache {
private static final String TAG = "ImageCache";
// Default memory cache size
private static final int DEFAULT_MEM_CACHE_SIZE = 1024 * 1024 * 5; // 5MB
// Default disk cache size
private static final int DEFAULT_DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
// Compression settings when writing images to disk cache
private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG;
private static final int DEFAULT_COMPRESS_QUALITY = 70;
// Constants to easily toggle various caches
private static final boolean DEFAULT_MEM_CACHE_ENABLED = true;
private static final boolean DEFAULT_DISK_CACHE_ENABLED = true;
private static final boolean DEFAULT_CLEAR_DISK_CACHE_ON_START = false;
private DiskLruCache mDiskCache;
private LruCache<String, Bitmap> mMemoryCache;
/**
* Creating a new ImageCache object using the specified parameters.
*
* @param context The context to use
* @param cacheParams The cache parameters to use to initialize the cache
*/
public ImageCache(Context context, ImageCacheParams cacheParams) {
init(context, cacheParams);
}
/**
* Creating a new ImageCache object using the default parameters.
*
* @param context The context to use
* @param uniqueName A unique name that will be appended to the cache directory
*/
public ImageCache(Context context, String uniqueName) {
init(context, new ImageCacheParams(uniqueName));
}
/**
* Find and return an existing ImageCache stored in a {@link RetainFragment}, if not found a new
* one is created with defaults and saved to a {@link RetainFragment}.
*
* @param activity The calling {@link FragmentActivity}
* @param uniqueName A unique name to append to the cache directory
* @return An existing retained ImageCache object or a new one if one did not exist.
*/
public static ImageCache findOrCreateCache(
final FragmentActivity activity, final String uniqueName) {
return findOrCreateCache(activity, new ImageCacheParams(uniqueName));
}
/**
* Find and return an existing ImageCache stored in a {@link RetainFragment}, if not found a new
* one is created using the supplied params and saved to a {@link RetainFragment}.
*
* @param activity The calling {@link FragmentActivity}
* @param cacheParams The cache parameters to use if creating the ImageCache
* @return An existing retained ImageCache object or a new one if one did not exist
*/
public static ImageCache findOrCreateCache(
final FragmentActivity activity, ImageCacheParams cacheParams) {
// Search for, or create an instance of the non-UI RetainFragment
final RetainFragment mRetainFragment = RetainFragment.findOrCreateRetainFragment(
activity.getSupportFragmentManager());
// See if we already have an ImageCache stored in RetainFragment
ImageCache imageCache = (ImageCache) mRetainFragment.getObject();
// No existing ImageCache, create one and store it in RetainFragment
if (imageCache == null) {
imageCache = new ImageCache(activity, cacheParams);
mRetainFragment.setObject(imageCache);
}
return imageCache;
}
/**
* Initialize the cache, providing all parameters.
*
* @param context The context to use
* @param cacheParams The cache parameters to initialize the cache
*/
private void init(Context context, ImageCacheParams cacheParams) {
final File diskCacheDir = DiskLruCache.getDiskCacheDir(context, cacheParams.uniqueName);
// Set up disk cache
if (cacheParams.diskCacheEnabled) {
mDiskCache = DiskLruCache.openCache(context, diskCacheDir, cacheParams.diskCacheSize);
mDiskCache.setCompressParams(cacheParams.compressFormat, cacheParams.compressQuality);
if (cacheParams.clearDiskCacheOnStart) {
mDiskCache.clearCache();
}
}
// Set up memory cache
if (cacheParams.memoryCacheEnabled) {
mMemoryCache = new LruCache<String, Bitmap>(cacheParams.memCacheSize) {
/**
* Measure item size in bytes rather than units which is more practical for a bitmap
* cache
*/
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return Utils.getBitmapSize(bitmap);
}
};
}
}
public void addBitmapToCache(String data, Bitmap bitmap) {
if (data == null || bitmap == null) {
return;
}
// Add to memory cache
if (mMemoryCache != null && mMemoryCache.get(data) == null) {
mMemoryCache.put(data, bitmap);
}
// Add to disk cache
if (mDiskCache != null && !mDiskCache.containsKey(data)) {
mDiskCache.put(data, bitmap);
}
}
/**
* Get from memory cache.
*
* @param data Unique identifier for which item to get
* @return The bitmap if found in cache, null otherwise
*/
public Bitmap getBitmapFromMemCache(String data) {
if (mMemoryCache != null) {
final Bitmap memBitmap = mMemoryCache.get(data);
if (memBitmap != null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Memory cache hit");
}
return memBitmap;
}
}
return null;
}
/**
* Get from disk cache.
*
* @param data Unique identifier for which item to get
* @return The bitmap if found in cache, null otherwise
*/
public Bitmap getBitmapFromDiskCache(String data) {
if (mDiskCache != null) {
return mDiskCache.get(data);
}
return null;
}
public void clearCaches() {
mDiskCache.clearCache();
mMemoryCache.evictAll();
}
/**
* A holder class that contains cache parameters.
*/
public static class ImageCacheParams {
public String uniqueName;
public int memCacheSize = DEFAULT_MEM_CACHE_SIZE;
public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE;
public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT;
public int compressQuality = DEFAULT_COMPRESS_QUALITY;
public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED;
public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED;
public boolean clearDiskCacheOnStart = DEFAULT_CLEAR_DISK_CACHE_ON_START;
public ImageCacheParams(String uniqueName) {
this.uniqueName = uniqueName;
}
}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.util.Log;
import android.widget.Toast;
import com.example.android.bitmapfun.BuildConfig;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* A simple subclass of {@link ImageResizer} that fetches and resizes images fetched from a URL.
*/
public class ImageFetcher extends ImageResizer {
private static final String TAG = "ImageFetcher";
private static final int HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10MB
public static final String HTTP_CACHE_DIR = "http";
/**
* Initialize providing a target image width and height for the processing images.
*
* @param context
* @param imageWidth
* @param imageHeight
*/
public ImageFetcher(Context context, int imageWidth, int imageHeight) {
super(context, imageWidth, imageHeight);
init(context);
}
/**
* Initialize providing a single target image size (used for both width and height);
*
* @param context
* @param imageSize
*/
public ImageFetcher(Context context, int imageSize) {
super(context, imageSize);
init(context);
}
private void init(Context context) {
checkConnection(context);
}
/**
* Simple network connection check.
*
* @param context
*/
private void checkConnection(Context context) {
final ConnectivityManager cm =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
if (networkInfo == null || !networkInfo.isConnectedOrConnecting()) {
Toast.makeText(context, "No network connection found.", Toast.LENGTH_LONG).show();
Log.e(TAG, "checkConnection - no connection found");
}
}
/**
* The main process method, which will be called by the ImageWorker in the AsyncTask background
* thread.
*
* @param data The data to load the bitmap, in this case, a regular http URL
* @return The downloaded and resized bitmap
*/
private Bitmap processBitmap(String data) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "processBitmap - " + data);
}
// Download a bitmap, write it to a file
final File f = downloadBitmap(mContext, data);
if (f != null) {
// Return a sampled down version
return decodeSampledBitmapFromFile(f.toString(), mImageWidth, mImageHeight);
}
return null;
}
@Override
protected Bitmap processBitmap(Object data) {
return processBitmap(String.valueOf(data));
}
/**
* Download a bitmap from a URL, write it to a disk and return the File pointer. This
* implementation uses a simple disk cache.
*
* @param context The context to use
* @param urlString The URL to fetch
* @return A File pointing to the fetched bitmap
*/
public static File downloadBitmap(Context context, String urlString) {
final File cacheDir = DiskLruCache.getDiskCacheDir(context, HTTP_CACHE_DIR);
final DiskLruCache cache =
DiskLruCache.openCache(context, cacheDir, HTTP_CACHE_SIZE);
final File cacheFile = new File(cache.createFilePath(urlString));
if (cache.containsKey(urlString)) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "downloadBitmap - found in http cache - " + urlString);
}
return cacheFile;
}
if (BuildConfig.DEBUG) {
Log.d(TAG, "downloadBitmap - downloading - " + urlString);
}
Utils.disableConnectionReuseIfNecessary();
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
final InputStream in =
new BufferedInputStream(urlConnection.getInputStream(), Utils.IO_BUFFER_SIZE);
out = new BufferedOutputStream(new FileOutputStream(cacheFile), Utils.IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return cacheFile;
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmap - " + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
if (out != null) {
try {
out.close();
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmap - " + e);
}
}
}
return null;
}
}

View File

@@ -0,0 +1,198 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.util;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
import com.example.android.bitmapfun.BuildConfig;
/**
* A simple subclass of {@link ImageWorker} that resizes images from resources given a target width
* and height. Useful for when the input images might be too large to simply load directly into
* memory.
*/
public class ImageResizer extends ImageWorker {
private static final String TAG = "ImageWorker";
protected int mImageWidth;
protected int mImageHeight;
/**
* Initialize providing a single target image size (used for both width and height);
*
* @param context
* @param imageWidth
* @param imageHeight
*/
public ImageResizer(Context context, int imageWidth, int imageHeight) {
super(context);
setImageSize(imageWidth, imageHeight);
}
/**
* Initialize providing a single target image size (used for both width and height);
*
* @param context
* @param imageSize
*/
public ImageResizer(Context context, int imageSize) {
super(context);
setImageSize(imageSize);
}
/**
* Set the target image width and height.
*
* @param width
* @param height
*/
public void setImageSize(int width, int height) {
mImageWidth = width;
mImageHeight = height;
}
/**
* Set the target image size (width and height will be the same).
*
* @param size
*/
public void setImageSize(int size) {
setImageSize(size, size);
}
/**
* The main processing method. This happens in a background task. In this case we are just
* sampling down the bitmap and returning it from a resource.
*
* @param resId
* @return
*/
private Bitmap processBitmap(int resId) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "processBitmap - " + resId);
}
return decodeSampledBitmapFromResource(
mContext.getResources(), resId, mImageWidth, mImageHeight);
}
@Override
protected Bitmap processBitmap(Object data) {
return processBitmap(Integer.parseInt(String.valueOf(data)));
}
/**
* Decode and sample down a bitmap from resources to the requested width and height.
*
* @param res The resources object containing the image data
* @param resId The resource id of the image data
* @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions
* that are equal to or greater than the requested width and height
*/
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
/**
* Decode and sample down a bitmap from a file to the requested width and height.
*
* @param filename The full path of the file to decode
* @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions
* that are equal to or greater than the requested width and height
*/
public static synchronized Bitmap decodeSampledBitmapFromFile(String filename,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filename, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(filename, options);
}
/**
* Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding
* bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates
* the closest inSampleSize that will result in the final decoded bitmap having a width and
* height equal to or larger than the requested width and height. This implementation does not
* ensure a power of 2 is returned for inSampleSize which can be faster when decoding but
* results in a larger bitmap which isn't as useful for caching purposes.
*
* @param options An options object with out* params already populated (run through a decode*
* method with inJustDecodeBounds==true
* @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap
* @return The value to be used for inSampleSize
*/
public static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
if (width > height) {
inSampleSize = Math.round((float) height / (float) reqHeight);
} else {
inSampleSize = Math.round((float) width / (float) reqWidth);
}
// This offers some additional logic in case the image has a strange
// aspect ratio. For example, a panorama may have a much larger
// width than height. In these cases the total pixels might still
// end up being too large to fit comfortably in memory, so we should
// be more aggressive with sample down the image (=larger
// inSampleSize).
final float totalPixels = width * height;
// Anything more than 2x the requested pixels we'll sample down
// further.
final float totalReqPixelsCap = reqWidth * reqHeight * 2;
while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
inSampleSize++;
}
}
return inSampleSize;
}
}

View File

@@ -0,0 +1,364 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.util;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.ImageView;
import com.example.android.bitmapfun.BuildConfig;
import java.lang.ref.WeakReference;
/**
* This class wraps up completing some arbitrary long running work when loading a bitmap to an
* ImageView. It handles things like using a memory and disk cache, running the work in a background
* thread and setting a placeholder image.
*/
public abstract class ImageWorker {
private static final String TAG = "ImageWorker";
private static final int FADE_IN_TIME = 200;
private ImageCache mImageCache;
private Bitmap mLoadingBitmap;
private boolean mFadeInBitmap = true;
private boolean mExitTasksEarly = false;
protected Context mContext;
protected ImageWorkerAdapter mImageWorkerAdapter;
protected ImageWorker(Context context) {
mContext = context;
}
/**
* Load an image specified by the data parameter into an ImageView (override
* {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and disk
* cache will be used if an {@link ImageCache} has been set using
* {@link ImageWorker#setImageCache(ImageCache)}. If the image is found in the memory cache, it
* is set immediately, otherwise an {@link AsyncTask} will be created to asynchronously load the
* bitmap.
*
* @param data The URL of the image to download.
* @param imageView The ImageView to bind the downloaded image to.
*/
public void loadImage(Object data, ImageView imageView) {
Bitmap bitmap = null;
if (mImageCache != null) {
bitmap = mImageCache.getBitmapFromMemCache(String.valueOf(data));
}
if (bitmap != null) {
// Bitmap found in memory cache
imageView.setImageBitmap(bitmap);
} else if (cancelPotentialWork(data, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(mContext.getResources(), mLoadingBitmap, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(data);
}
}
/**
* Load an image specified from a set adapter into an ImageView (override
* {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and disk
* cache will be used if an {@link ImageCache} has been set using
* {@link ImageWorker#setImageCache(ImageCache)}. If the image is found in the memory cache, it
* is set immediately, otherwise an {@link AsyncTask} will be created to asynchronously load the
* bitmap. {@link ImageWorker#setAdapter(ImageWorkerAdapter)} must be called before using this
* method.
*
* @param data The URL of the image to download.
* @param imageView The ImageView to bind the downloaded image to.
*/
public void loadImage(int num, ImageView imageView) {
if (mImageWorkerAdapter != null) {
loadImage(mImageWorkerAdapter.getItem(num), imageView);
} else {
throw new NullPointerException("Data not set, must call setAdapter() first.");
}
}
/**
* Set placeholder bitmap that shows when the the background thread is running.
*
* @param bitmap
*/
public void setLoadingImage(Bitmap bitmap) {
mLoadingBitmap = bitmap;
}
/**
* Set placeholder bitmap that shows when the the background thread is running.
*
* @param resId
*/
public void setLoadingImage(int resId) {
mLoadingBitmap = BitmapFactory.decodeResource(mContext.getResources(), resId);
}
/**
* Set the {@link ImageCache} object to use with this ImageWorker.
*
* @param cacheCallback
*/
public void setImageCache(ImageCache cacheCallback) {
mImageCache = cacheCallback;
}
public ImageCache getImageCache() {
return mImageCache;
}
/**
* If set to true, the image will fade-in once it has been loaded by the background thread.
*
* @param fadeIn
*/
public void setImageFadeIn(boolean fadeIn) {
mFadeInBitmap = fadeIn;
}
public void setExitTasksEarly(boolean exitTasksEarly) {
mExitTasksEarly = exitTasksEarly;
}
/**
* Subclasses should override this to define any processing or work that must happen to produce
* the final bitmap. This will be executed in a background thread and be long running. For
* example, you could resize a large bitmap here, or pull down an image from the network.
*
* @param data The data to identify which image to process, as provided by
* {@link ImageWorker#loadImage(Object, ImageView)}
* @return The processed bitmap
*/
protected abstract Bitmap processBitmap(Object data);
public static void cancelWork(ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
bitmapWorkerTask.cancel(true);
if (BuildConfig.DEBUG) {
final Object bitmapData = bitmapWorkerTask.data;
Log.d(TAG, "cancelWork - cancelled work for " + bitmapData);
}
}
}
/**
* Returns true if the current work has been canceled or if there was no work in
* progress on this image view.
* Returns false if the work in progress deals with the same data. The work is not
* stopped in that case.
*/
public static boolean cancelPotentialWork(Object data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final Object bitmapData = bitmapWorkerTask.data;
if (bitmapData == null || !bitmapData.equals(data)) {
bitmapWorkerTask.cancel(true);
if (BuildConfig.DEBUG) {
Log.d(TAG, "cancelPotentialWork - cancelled work for " + data);
}
} else {
// The same work is already in progress.
return false;
}
}
return true;
}
/**
* @param imageView Any imageView
* @return Retrieve the currently active work task (if any) associated with this imageView.
* null if there is no such task.
*/
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}
/**
* The actual AsyncTask that will asynchronously process the image.
*/
private class BitmapWorkerTask extends AsyncTask<Object, Void, Bitmap> {
private Object data;
private final WeakReference<ImageView> imageViewReference;
public BitmapWorkerTask(ImageView imageView) {
imageViewReference = new WeakReference<ImageView>(imageView);
}
/**
* Background processing.
*/
@Override
protected Bitmap doInBackground(Object... params) {
data = params[0];
final String dataString = String.valueOf(data);
Bitmap bitmap = null;
// If the image cache is available and this task has not been cancelled by another
// thread and the ImageView that was originally bound to this task is still bound back
// to this task and our "exit early" flag is not set then try and fetch the bitmap from
// the cache
if (mImageCache != null && !isCancelled() && getAttachedImageView() != null
&& !mExitTasksEarly) {
bitmap = mImageCache.getBitmapFromDiskCache(dataString);
}
// If the bitmap was not found in the cache and this task has not been cancelled by
// another thread and the ImageView that was originally bound to this task is still
// bound back to this task and our "exit early" flag is not set, then call the main
// process method (as implemented by a subclass)
if (bitmap == null && !isCancelled() && getAttachedImageView() != null
&& !mExitTasksEarly) {
bitmap = processBitmap(params[0]);
}
// If the bitmap was processed and the image cache is available, then add the processed
// bitmap to the cache for future use. Note we don't check if the task was cancelled
// here, if it was, and the thread is still running, we may as well add the processed
// bitmap to our cache as it might be used again in the future
if (bitmap != null && mImageCache != null) {
mImageCache.addBitmapToCache(dataString, bitmap);
}
return bitmap;
}
/**
* Once the image is processed, associates it to the imageView
*/
@Override
protected void onPostExecute(Bitmap bitmap) {
// if cancel was called on this task or the "exit early" flag is set then we're done
if (isCancelled() || mExitTasksEarly) {
bitmap = null;
}
final ImageView imageView = getAttachedImageView();
if (bitmap != null && imageView != null) {
setImageBitmap(imageView, bitmap);
}
}
/**
* Returns the ImageView associated with this task as long as the ImageView's task still
* points to this task as well. Returns null otherwise.
*/
private ImageView getAttachedImageView() {
final ImageView imageView = imageViewReference.get();
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (this == bitmapWorkerTask) {
return imageView;
}
return null;
}
}
/**
* A custom Drawable that will be attached to the imageView while the work is in progress.
* Contains a reference to the actual worker task, so that it can be stopped if a new binding is
* required, and makes sure that only the last started worker process can bind its result,
* independently of the finish order.
*/
private static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
/**
* Called when the processing is complete and the final bitmap should be set on the ImageView.
*
* @param imageView
* @param bitmap
*/
private void setImageBitmap(ImageView imageView, Bitmap bitmap) {
if (mFadeInBitmap) {
// Transition drawable with a transparent drwabale and the final bitmap
final TransitionDrawable td =
new TransitionDrawable(new Drawable[] {
new ColorDrawable(android.R.color.transparent),
new BitmapDrawable(mContext.getResources(), bitmap)
});
// Set background to loading bitmap
imageView.setBackgroundDrawable(
new BitmapDrawable(mContext.getResources(), mLoadingBitmap));
imageView.setImageDrawable(td);
td.startTransition(FADE_IN_TIME);
} else {
imageView.setImageBitmap(bitmap);
}
}
/**
* Set the simple adapter which holds the backing data.
*
* @param adapter
*/
public void setAdapter(ImageWorkerAdapter adapter) {
mImageWorkerAdapter = adapter;
}
/**
* Get the current adapter.
*
* @return
*/
public ImageWorkerAdapter getAdapter() {
return mImageWorkerAdapter;
}
/**
* A very simple adapter for use with ImageWorker class and subclasses.
*/
public static abstract class ImageWorkerAdapter {
public abstract Object getItem(int num);
public abstract int getSize();
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.util;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
/**
* A simple non-UI Fragment that stores a single Object and is retained over configuration changes.
* In this sample it will be used to retain the ImageCache object.
*/
public class RetainFragment extends Fragment {
private static final String TAG = "RetainFragment";
private Object mObject;
/**
* Empty constructor as per the Fragment documentation
*/
public RetainFragment() {}
/**
* Locate an existing instance of this Fragment or if not found, create and
* add it using FragmentManager.
*
* @param fm The FragmentManager manager to use.
* @return The existing instance of the Fragment or the new instance if just
* created.
*/
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
// Check to see if we have retained the worker fragment.
RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG);
// If not retained (or first time running), we need to create and add it.
if (mRetainFragment == null) {
mRetainFragment = new RetainFragment();
fm.beginTransaction().add(mRetainFragment, TAG).commit();
}
return mRetainFragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Make sure this Fragment is retained over a configuration change
setRetainInstance(true);
}
/**
* Store a single object in this Fragment.
*
* @param object The object to store
*/
public void setObject(Object object) {
mObject = object;
}
/**
* Get the stored object.
*
* @return The stored object
*/
public Object getObject() {
return mObject;
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.bitmapfun.util;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Environment;
import android.os.StatFs;
import java.io.File;
/**
* Class containing some static utility methods.
*/
public class Utils {
public static final int IO_BUFFER_SIZE = 8 * 1024;
private Utils() {};
/**
* Workaround for bug pre-Froyo, see here for more info:
* http://android-developers.blogspot.com/2011/09/androids-http-clients.html
*/
public static void disableConnectionReuseIfNecessary() {
// HTTP connection reuse which was buggy pre-froyo
if (hasHttpConnectionBug()) {
System.setProperty("http.keepAlive", "false");
}
}
/**
* Get the size in bytes of a bitmap.
* @param bitmap
* @return size in bytes
*/
@SuppressLint("NewApi")
public static int getBitmapSize(Bitmap bitmap) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
return bitmap.getByteCount();
}
// Pre HC-MR1
return bitmap.getRowBytes() * bitmap.getHeight();
}
/**
* Check if external storage is built-in or removable.
*
* @return True if external storage is removable (like an SD card), false
* otherwise.
*/
@SuppressLint("NewApi")
public static boolean isExternalStorageRemovable() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
return Environment.isExternalStorageRemovable();
}
return true;
}
/**
* Get the external app cache directory.
*
* @param context The context to use
* @return The external cache dir
*/
@SuppressLint("NewApi")
public static File getExternalCacheDir(Context context) {
if (hasExternalCacheDir()) {
return context.getExternalCacheDir();
}
// Before Froyo we need to construct the external cache dir ourselves
final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/";
return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir);
}
/**
* Check how much usable space is available at a given path.
*
* @param path The path to check
* @return The space available in bytes
*/
@SuppressLint("NewApi")
public static long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
return path.getUsableSpace();
}
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
/**
* Get the memory class of this device (approx. per-app memory limit)
*
* @param context
* @return
*/
public static int getMemoryClass(Context context) {
return ((ActivityManager) context.getSystemService(
Context.ACTIVITY_SERVICE)).getMemoryClass();
}
/**
* Check if OS version has a http URLConnection bug. See here for more information:
* http://android-developers.blogspot.com/2011/09/androids-http-clients.html
*
* @return
*/
public static boolean hasHttpConnectionBug() {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO;
}
/**
* Check if OS version has built-in external cache dir method.
*
* @return
*/
public static boolean hasExternalCacheDir() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO;
}
/**
* Check if ActionBar is available.
*
* @return
*/
public static boolean hasActionBar() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
}
}