├── .gitignore
├── .idea
├── compiler.xml
├── copyright
│ └── profiles_settings.xml
├── gradle.xml
├── misc.xml
├── modules.xml
└── runConfigurations.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── libs
│ ├── AndroidUtils.aar
│ ├── photoview-release-1.2.4.aar
│ ├── universal-image-loader-1.9.5-javadoc.jar
│ ├── universal-image-loader-1.9.5-sources.jar
│ └── universal-image-loader-1.9.5.jar
├── note
│ └── 00_AndroidAlbum开源项目实践小结.md
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── clock
│ │ └── album
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── clock
│ │ │ └── album
│ │ │ ├── AlbumApplication.java
│ │ │ ├── adapter
│ │ │ ├── AlbumFolderAdapter.java
│ │ │ └── AlbumGridAdapter.java
│ │ │ ├── crash
│ │ │ └── SimpleCrashReporter.java
│ │ │ ├── entity
│ │ │ ├── AlbumFolderInfo.java
│ │ │ └── ImageInfo.java
│ │ │ ├── imageloader
│ │ │ ├── ImageLoaderFactory.java
│ │ │ ├── ImageLoaderWrapper.java
│ │ │ └── UniversalAndroidImageLoader.java
│ │ │ ├── manager
│ │ │ └── FolderManager.java
│ │ │ ├── model
│ │ │ ├── ImageScannerModel.java
│ │ │ └── ImageScannerModelImpl.java
│ │ │ ├── presenter
│ │ │ ├── ImageScannerPresenter.java
│ │ │ ├── ImageScannerPresenterImpl.java
│ │ │ └── entity
│ │ │ │ └── ImageScanResult.java
│ │ │ ├── ui
│ │ │ ├── MainActivity.java
│ │ │ ├── activity
│ │ │ │ ├── AlbumActivity.java
│ │ │ │ ├── ImagePreviewActivity.java
│ │ │ │ ├── ImageSelectActivity.java
│ │ │ │ └── base
│ │ │ │ │ └── BaseActivity.java
│ │ │ ├── fragment
│ │ │ │ ├── AlbumDetailFragment.java
│ │ │ │ ├── AlbumFolderFragment.java
│ │ │ │ └── base
│ │ │ │ │ └── BaseFragment.java
│ │ │ └── widget
│ │ │ │ └── HackyViewPager.java
│ │ │ └── view
│ │ │ ├── AlbumView.java
│ │ │ ├── ImageChooseView.java
│ │ │ └── entity
│ │ │ └── AlbumViewData.java
│ └── res
│ │ ├── anim
│ │ ├── bottom_enter_anim.xml
│ │ ├── bottom_exit_anim.xml
│ │ ├── top_enter_anim.xml
│ │ └── top_exit_anim.xml
│ │ ├── drawable
│ │ └── image_selector.xml
│ │ ├── layout
│ │ ├── activity_album.xml
│ │ ├── activity_image_preview.xml
│ │ ├── activity_image_select.xml
│ │ ├── activity_main.xml
│ │ ├── album_directory_item.xml
│ │ ├── album_grid_item.xml
│ │ ├── fragment_album_detail.xml
│ │ ├── fragment_album_directory.xml
│ │ ├── preview_image_item.xml
│ │ └── selected_image_item.xml
│ │ ├── mipmap-hdpi
│ │ ├── group_item_arrow.png
│ │ ├── ic_launcher.png
│ │ ├── img_default.png
│ │ ├── img_error.png
│ │ └── navi_back_icon.png
│ │ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ ├── image_selected.png
│ │ └── image_unselected.png
│ │ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ └── ic_launcher.png
│ │ ├── values-v19
│ │ └── styles.xml
│ │ ├── values-v21
│ │ └── styles.xml
│ │ ├── values-w820dp
│ │ └── dimens.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── clock
│ └── album
│ └── ExampleUnitTest.java
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 | .externalNativeBuild
10 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AndroidAlbum
2 |
3 | 库如其名,做过企业的应用已经有三四个,但凡所有应用基本都有跳转到相册或者调用系统拍照的功能(例如所有应用都可以上传头像)。因此,为了方便公司或者自己的开发,抽空准备整理出一个比较完善的库,方便以后开发可以随时拉取代码。如果你对这部分的代码感兴趣,欢迎引入使用,如果引用过程中发现遇到什么闪退,麻烦在Github上给我提个issue,我会尽快定位修复。
4 |
5 | ## 最新更新(最后编辑于2016-11-08)
6 |
7 | - 重新整理项目结构,方便童鞋们导入运行。
8 |
9 | ## 目前已有功能
10 |
11 | - 展示系统所有带图片的目录,以及展示图片目录下所有图片
12 |
13 | - 点击图片预览大图功能,支持左右滑动切换和缩放功能
14 |
15 | - 闪退日志本地化存储功能,方便开发者本地查看
16 |
17 | - 腾讯bugly SDK的引入,用于上报crash的日志,方便远程定位错误
18 |
19 | - 图片预览界面添加了选图功能,预览页单击图片会出现沉浸模式(Immersive-Mode ,Android 4.4开始有的系统特性)
20 |
21 | ## 目前的效果
22 |
23 | - 本地图片选择功能
24 |
25 | 
26 |
27 | - 图片详情预览页面,添加选图功能和沉浸模式(Immersive-Mode)效果
28 |
29 | 
30 |
31 | ## 闪退日志处理
32 |
33 | **1.本地闪退日志处理**
34 |
35 | > 本地化存储闪退日志信息除了闪退的log外,还包含:设备厂商,设备名称,系统版本号,app版本号,设备id(IMEI)等。发生闪退后可以通过文件浏览器在SD卡上找到报错的log信息。(目前闪退日志是存放到SD下的album目录下的crash目录中。想要自己指定到其他目录的,可以在AlbumApplication中的configCollectCrashInfo函数)
36 |
37 | 闪退日志命名格式:发生闪退的时间(yyyyMMddHHmm 年月日时分秒).log
38 |
39 | 
40 |
41 | **2.闪退日志回传服务器处理**
42 |
43 | > 目前已经提供闪退日志回传到远程服务器的接口,有需要可以自行在AlbumApplication配置作如下实现!(发生闪退时,会回调onCrash方法,可以在此方法中讲闪退信息传回服务器)
44 |
45 | 
46 |
47 | **3.第三方上报crash功能的SDK引入**
48 |
49 | > 目前已经引入大鹅厂的[Bugly](http://bugly.qq.com/)(不得不佩服鹅厂的科技,真心牛逼)。这里引入第三方SDK仅仅只是为了跟踪一些BUG,并没有其他意图,不需要的童鞋可以自行移除掉。
50 |
51 | ## 引用第三方库
52 |
53 | - 图片加载框架:[Android-Universal-Image-Loader](https://github.com/nostra13/Android-Universal-Image-Loader)
54 |
55 | - 图片缩放控件:[PhotoView](https://github.com/chrisbanes/PhotoView)
56 |
57 | - 自己写的一个实用工具类库:[AndroidUtils](https://github.com/D-clock/AndroidUtils)
58 |
59 | ## 一些拓展处理
60 |
61 | - 为了方便项目的拓展,对引入的一些第三方库进行多加一层的抽象封装。如:当前库中引用的加载图片框架采用了[Android-Universal-Image-Loader](https://github.com/nostra13/Android-Universal-Image-Loader),为了降低项目对具体载图框架的依赖,特地使用工厂模式且加多了一层ImageLoaderWrapper对框架进行抽象解耦,这样为我后续替换其他加载图片框架节约了修改代码的成本。
62 |
63 | - 项目的编码设计采用了MVP架构,尽量的分离业务和UI,使得UI层的Activity和Fragment和业务层的代码显得松耦合。
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'bugly' //添加Bugly符号表插件
3 |
4 | android {
5 | compileSdkVersion 24
6 | buildToolsVersion "25.0.0"
7 | defaultConfig {
8 | applicationId "com.clock.album"
9 | minSdkVersion 15
10 | targetSdkVersion 24
11 | versionCode 1
12 | versionName "1.1"
13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
14 | ndk {
15 | // 设置支持的SO库架构
16 | abiFilters 'armeabi', 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
17 | }
18 | }
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | }
26 |
27 | repositories {
28 | flatDir {
29 | dirs 'libs' //this way we can find the .aar file in libs folder
30 | }
31 | }
32 |
33 | dependencies {
34 | compile fileTree(dir: 'libs', include: ['*.jar'])
35 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
36 | exclude group: 'com.android.support', module: 'support-annotations'
37 | })
38 | compile 'com.android.support:appcompat-v7:24.2.1'
39 | testCompile 'junit:junit:4.12'
40 | compile 'com.tencent.bugly:crashreport:latest.release'
41 | compile files('libs/universal-image-loader-1.9.5.jar')
42 | compile(name: 'photoview-release-1.2.4', ext: 'aar')
43 | compile(name: 'AndroidUtils', ext: 'aar')
44 | }
45 |
46 | bugly {
47 | appId = "900019014" //注册时分配的App ID
48 | appKey = "2XQMAyk12EBhkUUa" //注册时分配的App Key
49 | }
--------------------------------------------------------------------------------
/app/libs/AndroidUtils.aar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/D-clock/AndroidAlbum/27fc725451e384fee2c383c2ca0debbf9675ea61/app/libs/AndroidUtils.aar
--------------------------------------------------------------------------------
/app/libs/photoview-release-1.2.4.aar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/D-clock/AndroidAlbum/27fc725451e384fee2c383c2ca0debbf9675ea61/app/libs/photoview-release-1.2.4.aar
--------------------------------------------------------------------------------
/app/libs/universal-image-loader-1.9.5-javadoc.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/D-clock/AndroidAlbum/27fc725451e384fee2c383c2ca0debbf9675ea61/app/libs/universal-image-loader-1.9.5-javadoc.jar
--------------------------------------------------------------------------------
/app/libs/universal-image-loader-1.9.5-sources.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/D-clock/AndroidAlbum/27fc725451e384fee2c383c2ca0debbf9675ea61/app/libs/universal-image-loader-1.9.5-sources.jar
--------------------------------------------------------------------------------
/app/libs/universal-image-loader-1.9.5.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/D-clock/AndroidAlbum/27fc725451e384fee2c383c2ca0debbf9675ea61/app/libs/universal-image-loader-1.9.5.jar
--------------------------------------------------------------------------------
/app/note/00_AndroidAlbum开源项目实践小结.md:
--------------------------------------------------------------------------------
1 | # 我的 Android 项目开发实战经验总结
2 |
3 | 以前一直想写一篇总结 Android 开发经验的文章,估计当时的我还达不到某种水平,所以思路跟不上,下笔又捉襟见肘。近日,思路较为明朗,于是重新操起键盘开始码字一番。**先声明一下哈,本人不是大厂的程序猿。去年毕业前,就一直在当前创业小团队从事自己热爱的打码事业至今。下面总结是建立在我当前的技术水平和认知上写的,如有不同看法欢迎留下评论互相交流。**
4 |
5 | ## 1.理解抽象,封装变化
6 |
7 | 目前 Android 平台上绝大部分开发都是用着 Java ,而跟 Java 这样一门面向对象的语言打交道,不免要触碰到 **抽象** 和 **封装** 的概念。我身边接触过的一些开发者,有一部分还对这些概念停留在写一个抽象类、接口、或者一个方法(或抽象方法)。至于为什么,我不大清楚是他们表达不出来,还是不理解。下面我也不高谈阔论,直接举例子来解释我所理解的抽象。
8 |
9 | ```java
10 |
11 | //Activity 间使用 Intent 传递数据的两种写法 下面均是伪代码形式,请忽略一些细节
12 |
13 | //写法一
14 |
15 | //SrcActivity 传递数据给 DestActivity
16 | Intent intent = new Intent(this,DestActivity.class);
17 | intent.putExtra("param", "clock");
18 | SrcActivity.startActivity(intent);
19 |
20 | //DestActivity 获取 SrcActivity 传递过来的数据
21 | String param = getIntent.getStringExtra("param");
22 |
23 | //写法二
24 |
25 | //SrcActivity 传递数据给 DestActivity
26 | Intent intent = new Intent(this,DestActivity.class);
27 | intent.putExtra(DestActivity.EXTRA_PARAM, "clock");
28 | SrcActivity.startActivity(intent);
29 |
30 | //DestActivity 获取 SrcActivity 传递过来的数据
31 | public final static String EXTRA_PARAM = "param";
32 | String param = getIntent.getStringExtra(EXTRA_PARAM);
33 |
34 | ```
35 |
36 | 写法一,存在的问题是,如果 SrcActivity 和 DestActivity 哪个把 "param" 打错成 "para" 或者 "paran" ,传递的数据都无法成功接收到。而写法二则不会出现此类问题,因为两个 Activity 之间传递数据只需要知道 EXTRA_PARAM 变量即可,至于 EXTRA_PARAM 变量到底是 "param" 、 "para" 、"paran" 这一点并不需要关心,这就是一种对可能发生变化的地方进行抽象封装的体现,它所带来的好处就是降低手抖出错的概率,同时方便我们进行修改。
37 |
38 | 基于抽象和封装,Java 本身很多 API 在设计上就有这样的提现,如 Collections 中的很多排序方法:
39 |
40 | 
41 |
42 | 这些方法都是基于 List 这个抽象的列表接口进行排序,至于这是一个用什么样的数据结构实现 List(ArrayList 还是 LinkedList),排序方法本身并不关心。看,是不是体现了 JDK 的设计人员的一种抽象编程的思维,因为 List 的具体实现可能有千万种,如果每一类 List 都要写一套排序方法,估计要哭瞎了。
43 |
44 | > 小结:把容易出现变化的部分进行抽象,就是对变化的一种封装。
45 |
46 | ## 2.选好"车轮"
47 |
48 | 一个项目的开发,我们不可能一切从0做起,如果真是这样,那同样要哭瞎。因此,善于借用已经做好的 "车轮" 非常重要,如:
49 |
50 | 网络访问框架:okhttp、retrofit、android-async-http、volley
51 | 图片加载框架:Android-Universal-Image-Loader、Glide、Fresco、Picasso
52 | 缓存框架:DiskLruCache、 Robospice
53 | Json解析框架:Gson、Fastjson、Jackson
54 | 事件总线:EventBus、Otto
55 | ORM框架:GreenDAO、Litepal
56 | 第三方SDK:友盟统计,腾讯bugly、七牛...
57 | ...
58 | ...
59 | 还有其他各种各样开源的自定义控件、动画等。
60 |
61 | 一般情况下,我在选择是否引入一些开源框架主要基于以下几个因素:
62 |
63 | - 借助搜索引擎,如果网上有一大波资料,说明使用的人多,出了问题好找解决方案;当然,如果普遍出现差评,就可以直接Pass掉了
64 | - 看框架的作者或团队,如 [JakeWharton大神](https://github.com/JakeWharton)、[Facebook团队](https://code.facebook.com/)等。大神和大公司出品的框架质量相对较高,可保证后续的维护和bug修复,不容易烂尾;
65 | - 关注开源项目的 commit密度,issue的提交、回复、关闭数量,watch数,start数,fork数等。像那种个基本不怎么提交代码、提issue又不怎么回复和修复的项目,最好就pass掉;
66 |
67 | 针对第三方SDK的选择,也主要基于以下几点去考虑:
68 |
69 | - 借助搜索引擎,查明口碑;
70 | - 很多第三方SDK的官网首页都会告诉你,多少应用已经接入了此SDK,如果你看到有不少知名应用在上面,那这个SDK可以考虑尝试一下了。诸如,友盟官网:
71 |
72 | 
73 |
74 | - 查看SDK使用文档、它们的开发者社区、联系客服。好的SDK,使用文档肯定会详细指引你。出了问题,上开发者社区提问,他们的开发工程师也会社区上回答。实在不行只能联系客服,如果客服的态度都让你不爽,那就可以考虑换别家的SDK了。
75 |
76 | > 小结:选好 "车轮" ,事半功倍
77 |
78 | ## 3.抽象依赖第三方框架
79 |
80 | 为什么要抽象依赖于第三方框架呢?这里和第1点是互相照应的,就是降低我们对具体某个框架的依赖性,从而方便我们快速切换到不同的框架去。说到这里,你可能觉得很抽象,那我直接举一个加载图片的例子好了。
81 |
82 | 假设你当前为项目引入一个加载图片的框架 —— Android-Universal-Image-Loader,最简单的做法就是加入相应的依赖包后,在任何需要加载图片的地方写上下面这样的代码段。
83 |
84 | ```java
85 |
86 | ImageLoader imageLoader = ImageLoader.getInstance(); // Get singleton instance
87 | // Load image, decode it to Bitmap and display Bitmap in ImageView (or any other view
88 | // which implements ImageAware interface)
89 | imageLoader.displayImage(imageUri, imageView);
90 | // Load image, decode it to Bitmap and return Bitmap to callback
91 | imageLoader.loadImage(imageUri, new SimpleImageLoadingListener() {
92 | @Override
93 | public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
94 | // Do whatever you want with Bitmap
95 | }
96 | });
97 |
98 | ```
99 |
100 | 这种做法最简单粗暴,但是带来的问题也最严重的。如果我有几十上百个地方都这么写,而在某一天,我听说Facebook出了个神器 Fresco,想要换掉 Android-Universal-Image-Loader ,你就会发现你需要丧心病狂的去改动几十上百个地方的代码,不仅工作量大,而且还容易出错。造成这样的原因,就在于项目和加载图片的框架之间形成了**强耦合**,而实际上,项目本身不应该知道我具体用了哪个加载图片的框架。
101 |
102 | 正确的方式,应该是对框架做一个抽象的封装,以应对未来发生的变化,我直接举自己的开源项目 [AndroidAlbum](https://github.com/D-clock/AndroidAlbum) 中的一种封装做法好了。
103 |
104 | 
105 |
106 | 大致代码如下:
107 |
108 | ```java
109 | //1、声明 ImageLoaderWrapper 接口,定义一些抽象的加载接口方法
110 |
111 | public interface ImageLoaderWrapper {
112 |
113 | /**
114 | * 显示 图片
115 | *
116 | * @param imageView 显示图片的ImageView
117 | * @param imageFile 图片文件
118 | * @param option 显示参数设置
119 | */
120 | public void displayImage(ImageView imageView, File imageFile, DisplayOption option);
121 |
122 | /**
123 | * 显示图片
124 | *
125 | * @param imageView 显示图片的ImageView
126 | * @param imageUrl 图片资源的URL
127 | * @param option 显示参数设置
128 | */
129 | public void displayImage(ImageView imageView, String imageUrl, DisplayOption option);
130 |
131 | /**
132 | * 图片加载参数
133 | */
134 | public static class DisplayOption {
135 | /**
136 | * 加载中的资源id
137 | */
138 | public int loadingResId;
139 | /**
140 | * 加载失败的资源id
141 | */
142 | public int loadErrorResId;
143 | }
144 | }
145 |
146 | // 2、将 UniversalAndroidImageLoader 封装成继承 ImageLoaderWrapper 接口的 UniversalAndroidImageLoader ,
147 | //这里代码有点长,感兴趣可以查看项目源码中的实现 https://github.com/D-clock/AndroidAlbum
148 |
149 | // 3、做一个ImageLoaderFactory
150 |
151 | public class ImageLoaderFactory {
152 |
153 | private static ImageLoaderWrapper sInstance;
154 |
155 | private ImageLoaderFactory() {
156 |
157 | }
158 |
159 | /**
160 | * 获取图片加载器
161 | *
162 | * @return
163 | */
164 | public static ImageLoaderWrapper getLoader() {
165 | if (sInstance == null) {
166 | synchronized (ImageLoaderFactory.class) {
167 | if (sInstance == null) {
168 | sInstance = new UniversalAndroidImageLoader();//https://github.com/nostra13/Android-Universal-Image-Loader
169 | }
170 | }
171 | }
172 | return sInstance;
173 | }
174 | }
175 |
176 | //4、在所有需要加载图片的地方作如下的调用
177 |
178 | ImageLoaderWrapper loaderWrapper = ImageLoaderFactory.getLoader();
179 | ImageLoaderWrapper.DisplayOption displayOption = new ImageLoaderWrapper.DisplayOption();
180 | displayOption.loadingResId = R.mipmap.img_default;
181 | displayOption.loadErrorResId = R.mipmap.img_error;
182 | loaderWrapper.displayImage(imagview, url, displayOption);
183 |
184 | ```
185 |
186 | 这样一来,切换框架所带来的代价就会变得很小,这就是不直接依赖于框架所带来的好处。当然,以上只是我比较简单的封装,你也可以进行更加细致的处理。
187 |
188 | > 小结:预留变更,不强耦合于第三方框架
189 |
190 | ## 4.从 MVC 到 MVP
191 |
192 | 说实话,在没接触 MVP 的架构之前,一直都是使用 MVC 的模式进行开发。而随着项目越来越大,Activity或者 Fragment里面代码越来越臃肿,看的时候想吐,改的时候想屎...这里撇开其他各种各样的架构不谈,只对比MVC 和 MVP 。
193 |
194 | 
195 |
196 | - View:布局的xml文件
197 | - Controller:Activity、Fragment、Dialog等
198 | - Model:相关的业务操作处理数据(如对数据库的操作、对网络等的操作都应该在Model层里)
199 |
200 | 你会发现,如果 View 层只包含了xml文件,那我们 Android 项目中对 View 层可做操作的程度并不大,顶多就是用include复用一下布局。而 Activity 等简直就是一个奇葩,它虽然归属于 Controller 层,但实际上也干着 View 层的活(View 的初始化和相关操作都是在Activity中)。就是这种既是 View 又是 Controller 的结构,违背了单一责任原则,也使得 Activity 等出现了上述的臃肿问题。
201 |
202 | 
203 |
204 | - View:Activity、Fragment、Dialog、Adapter等,该层不包含任何业务逻辑
205 | - Presenter:中介,View 与 Model 不发生联系,都通过 Presenter 传递
206 | - Model:相关的业务操作处理数据(如对数据库的操作、对网络等的操作都应该在Model层里)
207 |
208 | 相比 MVC,MVP在层次划分上更加清晰了,不会出现一人身兼二职的情况(有些单元测试的童鞋,会发现单元测试用例更好写了)。在此处你可以看到 View 和 Model 之间是互不知道对方存在的,这样应对变更的好处更大,很多时候都是 View 层的变化,而 Model 层发生的变化会相对较少,遵循 MVP 的结构开发后,改起来代码来也没那么蛋疼。
209 | 这里也有地方需要注意,因为大量的交互操作集中在 Presenter 层中,所以需要把握好 Presenter 的粒度,一个 Activity 可以持有多个 View 和 Presenter,这样也就可以避开一个硕大的 View 和 Presenter 的问题了。
210 |
211 | 推荐两个不错的 MVP 架构的项目给大家,还不明白的童鞋,可以自行体会一下其设计思想:
212 |
213 | https://github.com/pedrovgs/EffectiveAndroidUI
214 | https://github.com/antoniolg/androidmvp
215 |
216 | > 小结:去加以实践的理解 MVP 吧
217 |
218 | ## 5.归档代码
219 |
220 | 把一些常用的工具类或业务流程代码进行归类整理,加入自己的代码库(还没有自己个人代码仓库的童鞋可以考虑建一个了)。如加解密、拍照、裁剪图片、获取系统所有图片的路径、自定义的控件或动画以及其其他他一些常用的工具类等。归档有助于提高你的开发效率,在遇到新项目的时候随手即可引入使用。如果你想要更好的维护自己的代码库,**不妨在不泄露公司机密的前提下,把这个私人代码库加上详细文档给开源出去。** 这样能够吸引更多开发者来使用这些代码,也可以获得相应的bug反馈,以便于着手定位修复问题,增强这个仓库代码的稳定性。
221 |
222 | > 小结:合理归档代码,可以的话,加以开源维护
223 |
224 | ## 6.性能优化
225 |
226 | 关于性能优化的问题,大体都还是关注那几个方面:内存、CPU、耗电、卡顿、渲染、进程存活率等。对于这些地方的性能优化思路和分析方法,网络上已经有很多答案了,此处不做赘述。我只想说以下几点:
227 |
228 | - 不要过早的做性能优化,app先求能用再求好用。在需求都还没完成的时候把大量时间花在优化上是本末倒置的;
229 | - 优化要用实际数据说话,借助测试工具进行检测(如:网易的Emmagee、腾讯的GT和APT,科大讯飞的iTest,Google的Battery Historian)。毕竟老板问你比以前耗电降低多少,总不能回答降低了一些吧???
230 | - 任何不以减低性能损耗来做保活的手段,都是耍流氓。
231 |
232 | > 小结:合理优化,数据量化
233 |
234 | ## 7.实践新技术
235 |
236 | Rxjava、React Native、Kotlin...开始兴起后,身边有很多开发者会跟风直上。学习新技术的精神是非常值得鼓励的,但没有经过一段时间实践观察,就擅自把新技术引入到商业项目中,则有失妥当。对于大公司的团队来说,会有专门团队或项目去研究这些新兴技术,以确定是否在自己的产品线开发中引入。但作为小公司,是不是就意味着没有实践尝试新技术的机会呢?并不是!个人有以下几点建议:
237 |
238 | - 借助搜索引擎。看此项技术坑多不多,口碑不错但是坑多的话,则说明当前技术不成熟,可以耐心等待更新;
239 | - 考虑学习成本。学习成本太大且不容易招到懂这方面的开发者的情况下,建议不要引入该技术;
240 | - 高仿一个项目并开源。如果你想引入 React Native 做商业开发,最好先高仿实现一个应用然后将其开源。这样一些对 RN 感兴趣的开发者会运行你的代码并反馈 bug 给你,有助于你知道一些新技术的坑,并寻找相应的解决方案,最终确定是否引入该技术;
241 | - 降低入门门槛。实践新技术的过程尽量加以详细的文档记录,这会有助于降低项目组其他同事对新技术的入门门槛,可以的话,也将学习文档开源,获得更多开发者对此份文档的反馈,也可纠正一些文档中的错误;
242 | - 结合实际业务。所有新技术的引入都要考虑是否符合当下的业务需求,我听过有些程序猿想引入新技术的原因是因为觉得这种技术很酷,网上说很好用,很啥啥啥...自己完全没弄过就人云亦云。有时候好无语,感觉在会用一些技术就像在炫技一样;
243 |
244 | > 小结:空谈误国,实干兴邦
245 |
246 | ## 8.UML
247 |
248 | UML,驯服代码和了解项目结构的利器,本人也在学习和体验其好处的路途上。不管遇到大小项目,有了它,可以更好的理清一些脉络结构。对付旧的庞大项目代码,或者有志阅读某些开源项目代码的开发者,绝对是居家必备。
249 |
250 | > 小结:工欲善其事,必先利其器
251 |
252 | ## 9.自造"车轮"
253 |
254 | 前面 2 提到,项目不可能从0开始,是需要引入很多第三方框架的。这里并不与 2 互相违背,而是建议有想提高技术逼格的开发者,可以在空暇时间去编码实现一个框架。如果你对网络访问、图片加载方面很有研究见解,不妨把这些脑海里的思想落实成具体的代码。也许你会发现,你动手去实践的时候,考虑的东西会多得多,自己最终得到的也会更多。(**特别建议那些看过很多开源代码,又至今未自己动手自撸一发的**)
255 |
256 | > 小结:不要停留在 api 调用的层面
257 |
258 | ## 10.扩大技术圈
259 |
260 | 有空又经济能力承受得起的时候,不妨去参加一些自己感兴趣的技术交流会。很多都有大牛上台演讲,听听人家的解决方案,拓宽一下自己看问题的思路,也可以多参加一些含金量高的线上活动。我有挺多开发者朋友,就是参加活动的时候认识的,有时候遇到一些技术问题,还会互相探讨交换一下解决思路。挺赞的!
261 |
262 | > 小结:拓宽技术视野
263 |
264 | ## 11.写博客总结
265 |
266 | 这个可能没什么好说的,大家看了标题就懂了。它最大的好处在于:
267 |
268 | - 系统化记录自己的解决方案;
269 | - 方便日后自己回顾;
270 | - 有问题也会有读者评论反馈,促进技术交流;
271 | - 增强自己书面表达能力;
272 |
273 | > 小结:认真总结,不断完善
274 |
275 | ## 12.找个对象
276 |
277 | 程序猿不要老是对着电脑,赶紧找个对象提升一下幸福感。据说幸福感高的程序猿,编码效率高,出bug几率小...
278 |
279 | > 总结:做个面向对象的程序员
280 |
281 |
282 | 大概就想到这些了,以后要是再有想写的,另开新篇。絮絮叨叨写了这么多,最关键的还是自己要落实,千万不要听说过太多道理,却依然过不好这一生哈!!!!
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in C:\Users\DW\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/clock/album/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.clock.album;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumentation test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.clock.album", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
32 |
36 |
40 |
41 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/AlbumApplication.java:
--------------------------------------------------------------------------------
1 | package com.clock.album;
2 |
3 | import android.app.Application;
4 | import android.util.Log;
5 |
6 | import com.clock.album.crash.SimpleCrashReporter;
7 | import com.clock.album.imageloader.UniversalAndroidImageLoader;
8 | import com.clock.album.manager.FolderManager;
9 | import com.clock.utils.crash.CrashExceptionHandler;
10 | import com.tencent.bugly.crashreport.CrashReport;
11 |
12 | /**
13 | * Created by Clock on 2016/1/17.
14 | */
15 | public class AlbumApplication extends Application {
16 |
17 |
18 | @Override
19 | public void onCreate() {
20 | super.onCreate();
21 |
22 | //此处用于配置本地生成闪退的日志文件,需要在其他第三方上报crash log类型的sdk初始化之前,
23 | // 进行初始化。否则会导致第三方的SDK无法上报crash log
24 | configCollectCrashInfo();
25 |
26 | initBuglyConfig();
27 |
28 | UniversalAndroidImageLoader.init(getApplicationContext());
29 |
30 | }
31 |
32 | /**
33 | * 配置奔溃信息的搜集
34 | */
35 | private void configCollectCrashInfo() {
36 | CrashExceptionHandler crashExceptionHandler = new CrashExceptionHandler(this, FolderManager.getCrashLogFolder());
37 | CrashExceptionHandler.CrashExceptionRemoteReport remoteReport = new SimpleCrashReporter();
38 | crashExceptionHandler.configRemoteReport(remoteReport); //设置友盟统计报错日志回传到远程服务器上
39 | Thread.setDefaultUncaughtExceptionHandler(crashExceptionHandler);
40 | }
41 |
42 | /**
43 | * 初始化bugly的设置(关于bugly的详细使用,可以看官方开发者文档)
44 | */
45 | private void initBuglyConfig() {
46 | CrashReport.initCrashReport(getApplicationContext(), "900019014", false);
47 | String buglyVersion = CrashReport.getBuglyVersion(getApplicationContext());
48 | Log.i("Bugly", "current bugly version: " + buglyVersion);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/adapter/AlbumFolderAdapter.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.adapter;
2 |
3 | import android.view.View;
4 | import android.view.ViewGroup;
5 | import android.widget.BaseAdapter;
6 | import android.widget.ImageView;
7 | import android.widget.TextView;
8 |
9 | import com.clock.album.R;
10 | import com.clock.album.entity.AlbumFolderInfo;
11 | import com.clock.album.entity.ImageInfo;
12 | import com.clock.album.imageloader.ImageLoaderWrapper;
13 |
14 | import java.io.File;
15 | import java.util.List;
16 |
17 | /**
18 | * 相册目录适配器
19 | *
20 | * Created by Clock on 2016/1/17.
21 | */
22 | public class AlbumFolderAdapter extends BaseAdapter {
23 |
24 | private List mAlbumFolderInfoList;
25 | private ImageLoaderWrapper mImageLoaderWrapper;
26 |
27 | public AlbumFolderAdapter(List albumFolderInfoList, ImageLoaderWrapper imageLoaderWrapper) {
28 | this.mAlbumFolderInfoList = albumFolderInfoList;
29 | this.mImageLoaderWrapper = imageLoaderWrapper;
30 | }
31 |
32 | @Override
33 | public int getCount() {
34 | if (mAlbumFolderInfoList == null) {
35 | return 0;
36 | }
37 | return mAlbumFolderInfoList.size();
38 | }
39 |
40 | @Override
41 | public Object getItem(int position) {
42 | return mAlbumFolderInfoList.get(position);
43 | }
44 |
45 | @Override
46 | public long getItemId(int position) {
47 | return position;
48 | }
49 |
50 | @Override
51 | public View getView(int position, View convertView, ViewGroup parent) {
52 | ViewHolder holder = null;
53 | if (convertView == null) {
54 | convertView = View.inflate(parent.getContext(), R.layout.album_directory_item, null);
55 | holder = new ViewHolder();
56 | holder.ivAlbumCover = (ImageView) convertView.findViewById(R.id.iv_album_cover);
57 | holder.tvDirectoryName = (TextView) convertView.findViewById(R.id.tv_directory_name);
58 | holder.tvChildCount = (TextView) convertView.findViewById(R.id.tv_child_count);
59 | convertView.setTag(holder);
60 |
61 | } else {
62 | holder = (ViewHolder) convertView.getTag();
63 |
64 | }
65 |
66 | AlbumFolderInfo albumFolderInfo = mAlbumFolderInfoList.get(position);
67 |
68 |
69 | File frontCover = albumFolderInfo.getFrontCover();
70 | ImageLoaderWrapper.DisplayOption displayOption = new ImageLoaderWrapper.DisplayOption();
71 | displayOption.loadingResId = R.mipmap.img_default;
72 | displayOption.loadErrorResId = R.mipmap.img_error;
73 | mImageLoaderWrapper.displayImage(holder.ivAlbumCover, frontCover, displayOption);
74 |
75 | String folderName = albumFolderInfo.getFolderName();
76 | holder.tvDirectoryName.setText(folderName);
77 |
78 | List imageInfoList = albumFolderInfo.getImageInfoList();
79 | holder.tvChildCount.setText(imageInfoList.size() + "");
80 |
81 | return convertView;
82 | }
83 |
84 | private static class ViewHolder {
85 | ImageView ivAlbumCover;
86 | TextView tvDirectoryName;
87 | TextView tvChildCount;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/adapter/AlbumGridAdapter.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.adapter;
2 |
3 | import android.view.View;
4 | import android.view.ViewGroup;
5 | import android.widget.AbsListView;
6 | import android.widget.BaseAdapter;
7 | import android.widget.CheckBox;
8 | import android.widget.CompoundButton;
9 | import android.widget.ImageView;
10 |
11 | import com.clock.album.R;
12 | import com.clock.album.entity.ImageInfo;
13 | import com.clock.album.imageloader.ImageLoaderWrapper;
14 | import com.clock.album.view.ImageChooseView;
15 | import com.clock.utils.common.RuleUtils;
16 |
17 | import java.util.List;
18 |
19 | /**
20 | * 相册视图适配器
21 | *
22 | * Created by Clock on 2016/1/16.
23 | */
24 | public class AlbumGridAdapter extends BaseAdapter {
25 |
26 | private List mImageInfoList;
27 | private ImageLoaderWrapper mImageLoaderWrapper;
28 | private View.OnClickListener mImageItemClickListener;
29 | private CompoundButton.OnCheckedChangeListener mImageOnSelectedListener;
30 | private OnClickPreviewImageListener mOnClickPreviewImageListener;
31 | private ImageChooseView mImageChooseView;
32 |
33 | public AlbumGridAdapter(List imageInfoList, ImageLoaderWrapper imageLoaderWrapper,
34 | ImageChooseView imageChooseView,
35 | OnClickPreviewImageListener onClickPreviewImageListener) {
36 | this.mImageInfoList = imageInfoList;
37 | this.mImageLoaderWrapper = imageLoaderWrapper;
38 | this.mImageChooseView = imageChooseView;
39 | this.mOnClickPreviewImageListener = onClickPreviewImageListener;
40 | }
41 |
42 | @Override
43 | public int getCount() {
44 | if (mImageInfoList == null) {
45 | return 0;
46 | }
47 | return mImageInfoList.size();
48 | }
49 |
50 | @Override
51 | public Object getItem(int position) {
52 | return mImageInfoList.get(position);
53 | }
54 |
55 | @Override
56 | public long getItemId(int position) {
57 | return position;
58 | }
59 |
60 | @Override
61 | public View getView(int position, View convertView, ViewGroup parent) {
62 | AlbumViewHolder holder = null;
63 | if (convertView == null) {
64 | holder = new AlbumViewHolder();
65 | convertView = View.inflate(parent.getContext(), R.layout.album_grid_item, null);
66 |
67 | int gridItemSpacing = (int) RuleUtils.convertDp2Px(parent.getContext(), 2);
68 | int gridEdgeLength = (RuleUtils.getScreenWidth(parent.getContext()) - gridItemSpacing * 2) / 3;
69 |
70 | AbsListView.LayoutParams layoutParams = new AbsListView.LayoutParams(gridEdgeLength, gridEdgeLength);
71 | convertView.setLayoutParams(layoutParams);
72 | holder.albumItem = (ImageView) convertView.findViewById(R.id.iv_album_item);
73 | holder.imageSelectedCheckBox = (CheckBox) convertView.findViewById(R.id.ckb_image_select);
74 | convertView.setTag(holder);
75 |
76 | } else {
77 | holder = (AlbumViewHolder) convertView.getTag();
78 | resetConvertView(holder);
79 |
80 | }
81 |
82 | ImageInfo imageInfo = mImageInfoList.get(position);
83 | ImageLoaderWrapper.DisplayOption displayOption = new ImageLoaderWrapper.DisplayOption();
84 | displayOption.loadingResId = R.mipmap.img_default;
85 | displayOption.loadErrorResId = R.mipmap.img_error;
86 | mImageLoaderWrapper.displayImage(holder.albumItem, imageInfo.getImageFile(), displayOption);
87 |
88 | holder.imageSelectedCheckBox.setChecked(imageInfo.isSelected());
89 | if (mImageOnSelectedListener == null) {
90 | mImageOnSelectedListener = new CompoundButton.OnCheckedChangeListener() {
91 |
92 | @Override
93 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
94 | ImageInfo imageInfo = (ImageInfo) buttonView.getTag();
95 | imageInfo.setIsSelected(isChecked);
96 | if (mImageChooseView != null) {
97 | mImageChooseView.refreshSelectedCounter(imageInfo);
98 | }
99 | }
100 | };
101 | }
102 | holder.imageSelectedCheckBox.setTag(imageInfo);
103 | holder.imageSelectedCheckBox.setOnCheckedChangeListener(mImageOnSelectedListener);//监听图片是否被选中的状态
104 |
105 | if (mImageItemClickListener == null) {
106 | mImageItemClickListener = new View.OnClickListener() {
107 | @Override
108 | public void onClick(View v) {
109 | ImageInfo imageInfo = (ImageInfo) v.getTag();
110 | if (mOnClickPreviewImageListener != null) {
111 | mOnClickPreviewImageListener.onClickPreview(imageInfo);
112 | }
113 | }
114 | };
115 | }
116 |
117 | holder.albumItem.setTag(imageInfo);
118 | holder.albumItem.setOnClickListener(mImageItemClickListener);
119 |
120 | return convertView;
121 | }
122 |
123 | /**
124 | * 重置缓存视图的初始状态
125 | *
126 | * @param viewHolder
127 | */
128 | private void resetConvertView(AlbumViewHolder viewHolder) {
129 | viewHolder.imageSelectedCheckBox.setOnCheckedChangeListener(null);//先取消选择状态的监听
130 | viewHolder.imageSelectedCheckBox.setChecked(false);
131 | }
132 |
133 | private static class AlbumViewHolder {
134 | /**
135 | * 显示图片的位置
136 | */
137 | ImageView albumItem;
138 | /**
139 | * 图片选择按钮
140 | */
141 | CheckBox imageSelectedCheckBox;
142 | }
143 |
144 |
145 | /**
146 | * 点击预览图片操作监听借口
147 | */
148 | public static interface OnClickPreviewImageListener {
149 | /**
150 | * 当想点击某张图片进行预览的时候触发此函数
151 | *
152 | * @param imageInfo
153 | */
154 | public void onClickPreview(ImageInfo imageInfo);
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/crash/SimpleCrashReporter.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.crash;
2 |
3 | import com.clock.utils.crash.CrashExceptionHandler;
4 |
5 | /**
6 | * 自定义的回传闪退日志到远程服务器
7 | *
8 | * Created by Clock on 2016/1/27.
9 | */
10 | public class SimpleCrashReporter implements CrashExceptionHandler.CrashExceptionRemoteReport {
11 |
12 | @Override
13 | public void onCrash(Throwable ex) {
14 | //接下来要在此处加入将闪退日志回传到服务器的功能
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/entity/AlbumFolderInfo.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.entity;
2 |
3 | import java.io.File;
4 | import java.io.Serializable;
5 | import java.util.List;
6 |
7 | /**
8 | * 目录信息
9 | *
10 | * Created by Clock on 2016/3/21.
11 | */
12 | public class AlbumFolderInfo implements Serializable {
13 |
14 |
15 | /**
16 | * 目录名
17 | */
18 | private String folderName;
19 | /**
20 | * 包含的所有图片信息
21 | */
22 | private List imageInfoList;
23 | /**
24 | * 第一张图片
25 | */
26 | private File frontCover;
27 |
28 | public File getFrontCover() {
29 | return frontCover;
30 | }
31 |
32 | public void setFrontCover(File frontCover) {
33 | this.frontCover = frontCover;
34 | }
35 |
36 | public String getFolderName() {
37 | return folderName;
38 | }
39 |
40 | public void setFolderName(String folderName) {
41 | this.folderName = folderName;
42 | }
43 |
44 | public List getImageInfoList() {
45 | return imageInfoList;
46 | }
47 |
48 | public void setImageInfoList(List imageInfoList) {
49 | this.imageInfoList = imageInfoList;
50 | }
51 |
52 | @Override
53 | public boolean equals(Object o) {
54 | if (this == o) return true;
55 | if (o == null || getClass() != o.getClass()) return false;
56 |
57 | AlbumFolderInfo that = (AlbumFolderInfo) o;
58 |
59 | if (getFolderName() != null ? !getFolderName().equals(that.getFolderName()) : that.getFolderName() != null)
60 | return false;
61 | if (getImageInfoList() != null ? !getImageInfoList().equals(that.getImageInfoList()) : that.getImageInfoList() != null)
62 | return false;
63 | return !(getFrontCover() != null ? !getFrontCover().equals(that.getFrontCover()) : that.getFrontCover() != null);
64 |
65 | }
66 |
67 | @Override
68 | public int hashCode() {
69 | int result = getFolderName() != null ? getFolderName().hashCode() : 0;
70 | result = 31 * result + (getImageInfoList() != null ? getImageInfoList().hashCode() : 0);
71 | result = 31 * result + (getFrontCover() != null ? getFrontCover().hashCode() : 0);
72 | return result;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/entity/ImageInfo.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.entity;
2 |
3 | import java.io.File;
4 | import java.io.Serializable;
5 | import java.util.ArrayList;
6 | import java.util.List;
7 |
8 | /**
9 | * 图片信息
10 | *
11 | * Created by Clock on 2016/1/26.
12 | */
13 | public class ImageInfo implements Serializable {
14 |
15 | private static final long serialVersionUID = -3753345306395582567L;
16 | /**
17 | * 图片文件
18 | */
19 | private File imageFile;
20 | /**
21 | * 是否被选中
22 | */
23 | private boolean isSelected = false;
24 |
25 | public File getImageFile() {
26 | return imageFile;
27 | }
28 |
29 | public void setImageFile(File imageFile) {
30 | this.imageFile = imageFile;
31 | }
32 |
33 | public boolean isSelected() {
34 | return isSelected;
35 | }
36 |
37 | public void setIsSelected(boolean isSelected) {
38 | this.isSelected = isSelected;
39 | }
40 |
41 | @Override
42 | public boolean equals(Object o) {
43 | if (this == o) return true;
44 | if (o == null || getClass() != o.getClass()) return false;
45 |
46 | ImageInfo imageInfo = (ImageInfo) o;
47 |
48 | if (isSelected() != imageInfo.isSelected()) return false;
49 | return getImageFile().equals(imageInfo.getImageFile());
50 |
51 | }
52 |
53 | @Override
54 | public int hashCode() {
55 | int result = getImageFile().hashCode();
56 | result = 31 * result + (isSelected() ? 1 : 0);
57 | return result;
58 | }
59 |
60 | /**
61 | * @param imageFileList
62 | * @return
63 | */
64 | public static List buildFromFileList(List imageFileList) {
65 | if (imageFileList != null) {
66 | List imageInfoArrayList = new ArrayList<>();
67 | for (File imageFile : imageFileList) {
68 | ImageInfo imageInfo = new ImageInfo();
69 | imageInfo.imageFile = imageFile;
70 | imageInfo.isSelected = false;
71 | imageInfoArrayList.add(imageInfo);
72 | }
73 | return imageInfoArrayList;
74 | } else {
75 | return null;
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/imageloader/ImageLoaderFactory.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.imageloader;
2 |
3 | /**
4 | * ImageLoader工厂类
5 | *
6 | * Created by Clock on 2016/1/18.
7 | */
8 | public class ImageLoaderFactory {
9 |
10 | private static volatile ImageLoaderWrapper sInstance;
11 |
12 | private ImageLoaderFactory() {
13 |
14 | }
15 |
16 | /**
17 | * 获取图片加载器
18 | *
19 | * @return
20 | */
21 | public static ImageLoaderWrapper getLoader() {
22 | if (sInstance == null) {
23 | synchronized (ImageLoaderFactory.class) {
24 | if (sInstance == null) {
25 | sInstance = new UniversalAndroidImageLoader();//https://github.com/nostra13/Android-Universal-Image-Loader
26 | }
27 | }
28 | }
29 | return sInstance;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/imageloader/ImageLoaderWrapper.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.imageloader;
2 |
3 | import android.widget.ImageView;
4 |
5 | import java.io.File;
6 |
7 | /**
8 | * 图片加载功能接口
9 | *
10 | * Created by Clock on 2016/1/18.
11 | */
12 | public interface ImageLoaderWrapper {
13 |
14 | /**
15 | * 显示 图片
16 | *
17 | * @param imageView 显示图片的ImageView
18 | * @param imageFile 图片文件
19 | * @param option 显示参数设置
20 | */
21 | public void displayImage(ImageView imageView, File imageFile, DisplayOption option);
22 |
23 | /**
24 | * 显示图片
25 | *
26 | * @param imageView 显示图片的ImageView
27 | * @param imageUrl 图片资源的URL
28 | * @param option 显示参数设置
29 | */
30 | public void displayImage(ImageView imageView, String imageUrl, DisplayOption option);
31 |
32 | /**
33 | * 图片加载参数
34 | */
35 | public static class DisplayOption {
36 | /**
37 | * 加载中的资源id
38 | */
39 | public int loadingResId;
40 | /**
41 | * 加载失败的资源id
42 | */
43 | public int loadErrorResId;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/imageloader/UniversalAndroidImageLoader.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.imageloader;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.widget.ImageView;
6 |
7 | import com.clock.album.R;
8 | import com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator;
9 | import com.nostra13.universalimageloader.core.DisplayImageOptions;
10 | import com.nostra13.universalimageloader.core.ImageLoader;
11 | import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
12 | import com.nostra13.universalimageloader.core.assist.QueueProcessingType;
13 | import com.nostra13.universalimageloader.core.download.ImageDownloader;
14 |
15 | import java.io.File;
16 |
17 | /**
18 | * 开源框架 Android-Universal-Image-Loader 的封装实现
19 | *
20 | * https://github.com/nostra13/Android-Universal-Image-Loader
21 | * Created by Clock on 2016/1/18.
22 | */
23 | public class UniversalAndroidImageLoader implements ImageLoaderWrapper {
24 |
25 | private final static String HTTP = "http";
26 | private final static String HTTPS = "https";
27 |
28 | UniversalAndroidImageLoader() {
29 |
30 | }
31 |
32 | @Override
33 | public void displayImage(ImageView imageView, File imageFile, DisplayOption option) {
34 | int imageLoadingResId = R.mipmap.img_default;
35 | int imageErrorResId = R.mipmap.img_error;
36 | if (option != null) {
37 | imageLoadingResId = option.loadingResId;
38 | imageErrorResId = option.loadErrorResId;
39 | }
40 | DisplayImageOptions options = new DisplayImageOptions.Builder()
41 | .showImageOnLoading(imageLoadingResId)
42 | .showImageForEmptyUri(imageErrorResId)
43 | .showImageOnFail(imageErrorResId)
44 | .cacheInMemory(true) //加载本地图片不需要再做SD卡缓存,只做内存缓存即可
45 | .considerExifParams(true)
46 | .bitmapConfig(Bitmap.Config.RGB_565)
47 | .build();
48 | String uri;
49 | if (imageFile == null) {
50 | uri = "";
51 | } else {
52 | uri = ImageDownloader.Scheme.FILE.wrap(imageFile.getAbsolutePath());
53 | }
54 | ImageLoader.getInstance().displayImage(uri, imageView, options);
55 | }
56 |
57 | @Override
58 | public void displayImage(ImageView imageView, String imageUrl, DisplayOption option) {
59 | int imageLoadingResId = R.mipmap.img_default;
60 | int imageErrorResId = R.mipmap.img_error;
61 | if (option != null) {
62 | imageLoadingResId = option.loadingResId;
63 | imageErrorResId = option.loadErrorResId;
64 | }
65 | DisplayImageOptions options = new DisplayImageOptions.Builder()
66 | .showImageOnLoading(imageLoadingResId)
67 | .showImageForEmptyUri(imageErrorResId)
68 | .showImageOnFail(imageErrorResId)
69 | .cacheInMemory(true)
70 | .cacheOnDisk(true)
71 | .considerExifParams(true)
72 | .bitmapConfig(Bitmap.Config.RGB_565)
73 | .build();
74 | if (imageUrl.startsWith(HTTPS)) {
75 | String uri = ImageDownloader.Scheme.HTTPS.wrap(imageUrl);
76 | ImageLoader.getInstance().displayImage(uri, imageView, options);
77 | } else if (imageUrl.startsWith(HTTP)) {
78 | String uri = ImageDownloader.Scheme.HTTP.wrap(imageUrl);
79 | ImageLoader.getInstance().displayImage(uri, imageView, options);
80 | }
81 | }
82 |
83 | /**
84 | * 初始化Universal-Image-Loader框架的参数设置
85 | *
86 | * @param context
87 | */
88 | public static void init(Context context) {
89 | // This configuration tuning is custom. You can tune every option, you may tune some of them,
90 | // or you can create default configuration by
91 | // ImageLoaderConfiguration.createDefault(this);
92 | // method.
93 | ImageLoaderConfiguration.Builder config = new ImageLoaderConfiguration.Builder(context);
94 | config.threadPriority(Thread.NORM_PRIORITY - 2);
95 | config.denyCacheImageMultipleSizesInMemory();
96 | config.diskCacheFileNameGenerator(new Md5FileNameGenerator());
97 | config.diskCacheSize(50 * 1024 * 1024); // 50 MiB
98 | config.tasksProcessingOrder(QueueProcessingType.LIFO);
99 | config.writeDebugLogs(); // Remove for release app
100 |
101 | // Initialize ImageLoader with configuration.
102 | ImageLoader.getInstance().init(config.build());
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/manager/FolderManager.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.manager;
2 |
3 | import android.os.Environment;
4 |
5 | import java.io.File;
6 |
7 | /**
8 | * 目录管理器
9 | *
10 | * Created by Clock on 2016/5/28.
11 | */
12 | public class FolderManager {
13 |
14 | /**
15 | * 应用程序在SD卡上的主目录名称
16 | */
17 | private final static String APP_FOLDER_NAME = "album";
18 | /**
19 | * 存放闪退日志目录名
20 | */
21 | private final static String CRASH_LOG_FOLDER_NAME = "crash";
22 |
23 | private FolderManager() {
24 | }
25 |
26 | /**
27 | * 获取app在sd卡上的主目录
28 | *
29 | * @return 成功则返回目录,失败则返回null
30 | */
31 | public static File getAppFolder() {
32 | if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
33 |
34 | File appFolder = new File(Environment.getExternalStorageDirectory(), APP_FOLDER_NAME);
35 | return createOnNotFound(appFolder);
36 |
37 | } else {
38 | return null;
39 | }
40 | }
41 |
42 | /**
43 | * 获取闪退日志存放目录
44 | *
45 | * @return
46 | */
47 | public static File getCrashLogFolder() {
48 | File appFolder = getAppFolder();
49 | if (appFolder != null) {
50 |
51 | File crashLogFolder = new File(appFolder, CRASH_LOG_FOLDER_NAME);
52 | return createOnNotFound(crashLogFolder);
53 | } else {
54 | return null;
55 | }
56 | }
57 |
58 | /**
59 | * 创建目录
60 | *
61 | * @param folder
62 | * @return 创建成功则返回目录,失败则返回null
63 | */
64 | private static File createOnNotFound(File folder) {
65 | if (folder == null) {
66 | return null;
67 | }
68 |
69 | if (!folder.exists()) {
70 | folder.mkdirs();
71 | }
72 |
73 | if (folder.exists()) {
74 | return folder;
75 | } else {
76 | return null;
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/model/ImageScannerModel.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.model;
2 |
3 | import android.content.Context;
4 | import android.support.v4.app.LoaderManager;
5 |
6 | import com.clock.album.presenter.entity.ImageScanResult;
7 | import com.clock.album.view.entity.AlbumViewData;
8 |
9 | /**
10 | * 图片扫描Model层接口
11 | *
12 | * Created by Clock on 2016/3/19.
13 | */
14 | public interface ImageScannerModel {
15 |
16 | /**
17 | * 获取所有图片的信息列表(图片目录的绝对路径作为map的key,value是该图片目录下的所有图片文件信息)
18 | *
19 | * @param context
20 | * @param loaderManager
21 | * @param onScanImageFinish 扫描图片结束返回结果的回调接口
22 | * @return
23 | */
24 | public void startScanImage(Context context, LoaderManager loaderManager, OnScanImageFinish onScanImageFinish);
25 |
26 | /**
27 | * 归档整理相册信息
28 | *
29 | * @param imageScanResult
30 | * @return 整理好的相册目录信息
31 | */
32 | public AlbumViewData archiveAlbumInfo(Context context, ImageScanResult imageScanResult);
33 |
34 | /**
35 | * 图片扫描结果回调接口
36 | */
37 | public static interface OnScanImageFinish {
38 |
39 | /**
40 | * 扫描结束的时候执行此函数
41 | *
42 | * @param imageScanResult 返回扫描结果,不存在图片则返回null
43 | */
44 | public void onFinish(ImageScanResult imageScanResult);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/model/ImageScannerModelImpl.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.model;
2 |
3 | import android.content.Context;
4 | import android.database.Cursor;
5 | import android.os.Bundle;
6 | import android.os.Handler;
7 | import android.os.Message;
8 | import android.provider.MediaStore;
9 | import android.support.v4.app.LoaderManager;
10 | import android.support.v4.content.CursorLoader;
11 | import android.support.v4.content.Loader;
12 | import android.util.Log;
13 |
14 | import com.clock.album.R;
15 | import com.clock.album.entity.AlbumFolderInfo;
16 | import com.clock.album.entity.ImageInfo;
17 | import com.clock.album.presenter.entity.ImageScanResult;
18 | import com.clock.album.view.entity.AlbumViewData;
19 |
20 | import java.io.File;
21 | import java.util.ArrayList;
22 | import java.util.Collections;
23 | import java.util.Comparator;
24 | import java.util.HashMap;
25 | import java.util.List;
26 | import java.util.Map;
27 | import java.util.Set;
28 |
29 | /**
30 | * Created by Clock on 2016/3/21.
31 | */
32 | public class ImageScannerModelImpl implements ImageScannerModel {
33 |
34 | private final static String TAG = ImageScannerModelImpl.class.getSimpleName();
35 | /**
36 | * Loader的唯一ID号
37 | */
38 | private final static int IMAGE_LOADER_ID = 1000;
39 | /**
40 | * 加载数据的映射
41 | */
42 | private final static String[] IMAGE_PROJECTION = new String[]{
43 | MediaStore.Images.Media.DATA,//图片路径
44 | MediaStore.Images.Media.DISPLAY_NAME,//图片文件名,包括后缀名
45 | MediaStore.Images.Media.TITLE//图片文件名,不包含后缀
46 | };
47 |
48 | private OnScanImageFinish mOnScanImageFinish;
49 |
50 | private Handler mRefreshHandler = new Handler() {
51 | @Override
52 | public void handleMessage(Message msg) {
53 | ImageScanResult imageScanResult = (ImageScanResult) msg.obj;
54 | if (mOnScanImageFinish != null && imageScanResult != null) {
55 | mOnScanImageFinish.onFinish(imageScanResult);
56 | }
57 | }
58 | };
59 |
60 | @Override
61 | public void startScanImage(final Context context, LoaderManager loaderManager, final OnScanImageFinish onScanImageFinish) {
62 | mOnScanImageFinish = onScanImageFinish;
63 | LoaderManager.LoaderCallbacks loaderCallbacks = new LoaderManager.LoaderCallbacks() {
64 | @Override
65 | public Loader onCreateLoader(int id, Bundle args) {
66 | Log.i(TAG, "-----onCreateLoader-----");
67 | CursorLoader imageCursorLoader = new CursorLoader(context, MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
68 | IMAGE_PROJECTION, null, null, MediaStore.Images.Media.DEFAULT_SORT_ORDER);
69 | return imageCursorLoader;
70 | }
71 |
72 | @Override
73 | public void onLoadFinished(Loader loader, Cursor data) {
74 | Log.i(TAG, "-----onLoadFinished-----");
75 | if (data.getCount() == 0) {
76 | if (onScanImageFinish != null) {
77 | onScanImageFinish.onFinish(null);//无图片直接返回null
78 | }
79 |
80 | } else {
81 | int dataColumnIndex = data.getColumnIndex(MediaStore.Images.Media.DATA);
82 | //int displayNameColumnIndex = data.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME);
83 | //int titleColumnIndex = data.getColumnIndex(MediaStore.Images.Media.TITLE);
84 | ArrayList albumFolderList = new ArrayList<>();
85 | HashMap> albumImageListMap = new HashMap<>();
86 | while (data.moveToNext()) {
87 | File imageFile = new File(data.getString(dataColumnIndex));//图片文件
88 | File albumFolder = imageFile.getParentFile();//图片目录
89 | if (!albumFolderList.contains(albumFolder)) {
90 | albumFolderList.add(albumFolder);
91 | }
92 | String albumPath = albumFolder.getAbsolutePath();
93 | ArrayList albumImageFiles = albumImageListMap.get(albumPath);
94 | if (albumImageFiles == null) {
95 | albumImageFiles = new ArrayList<>();
96 | albumImageListMap.put(albumPath, albumImageFiles);
97 | }
98 | albumImageFiles.add(imageFile);//添加到对应的相册目录下面
99 | }
100 |
101 | sortByFileLastModified(albumFolderList);//对图片目录做排序
102 |
103 | Set keySet = albumImageListMap.keySet();
104 | for (String key : keySet) {//对图片目录下所有的图片文件做排序
105 | ArrayList albumImageList = albumImageListMap.get(key);
106 | sortByFileLastModified(albumImageList);
107 | }
108 |
109 | ImageScanResult imageScanResult = new ImageScanResult();
110 | imageScanResult.setAlbumFolderList(albumFolderList);
111 | imageScanResult.setAlbumImageListMap(albumImageListMap);
112 |
113 | //Fix CursorLoader Bug
114 | //http://stackoverflow.com/questions/7746140/android-problems-using-fragmentactivity-loader-to-update-fragmentstatepagera
115 | Message message = mRefreshHandler.obtainMessage();
116 | message.obj = imageScanResult;
117 | mRefreshHandler.sendMessage(message);
118 |
119 | }
120 |
121 | }
122 |
123 | @Override
124 | public void onLoaderReset(Loader loader) {
125 | Log.i(TAG, "-----onLoaderReset-----");
126 | }
127 | };
128 | loaderManager.initLoader(IMAGE_LOADER_ID, null, loaderCallbacks);//初始化指定id的Loader
129 | }
130 |
131 | @Override
132 | public AlbumViewData archiveAlbumInfo(Context context, ImageScanResult imageScanResult) {
133 | if (imageScanResult != null) {
134 |
135 | List albumFolderList = imageScanResult.getAlbumFolderList();
136 | Map> albumImageListMap = imageScanResult.getAlbumImageListMap();
137 |
138 | if (albumFolderList != null && albumFolderList.size() > 0 && albumImageListMap != null) {
139 |
140 | List albumFolderInfoList = new ArrayList<>();
141 |
142 | AlbumFolderInfo allImageFolder = createAllImageAlbum(context, albumImageListMap);
143 | if (allImageFolder != null) {
144 | albumFolderInfoList.add(allImageFolder);
145 | }
146 |
147 | int albumFolderSize = albumFolderList.size();
148 | for (int albumFolderPos = 0; albumFolderPos < albumFolderSize; albumFolderPos++) {
149 |
150 | File albumFolder = albumFolderList.get(albumFolderPos);
151 | AlbumFolderInfo albumFolderInfo = new AlbumFolderInfo();
152 |
153 | String folderName = albumFolder.getName();
154 | albumFolderInfo.setFolderName(folderName);
155 |
156 | String albumPath = albumFolder.getAbsolutePath();
157 | List albumImageList = albumImageListMap.get(albumPath);
158 | File frontCover = albumImageList.get(0);
159 | albumFolderInfo.setFrontCover(frontCover);//设置首张图片
160 |
161 | List imageInfoList = ImageInfo.buildFromFileList(albumImageList);
162 | albumFolderInfo.setImageInfoList(imageInfoList);
163 | allImageFolder.getImageInfoList().addAll(imageInfoList);//保存到 "全部图片" 目录下
164 |
165 | albumFolderInfoList.add(albumFolderInfo);
166 | }
167 |
168 | AlbumViewData albumViewData = new AlbumViewData();
169 | albumViewData.setAlbumFolderInfoList(albumFolderInfoList);
170 |
171 | return albumViewData;
172 | }
173 |
174 | return null;
175 | } else {
176 | return null;
177 | }
178 | }
179 |
180 | /**
181 | * 创建一个"全部图片"目录
182 | *
183 | * @param albumImageListMap
184 | * @return
185 | */
186 | private AlbumFolderInfo createAllImageAlbum(Context context, Map> albumImageListMap) {
187 | if (albumImageListMap != null) {
188 | AlbumFolderInfo albumFolderInfo = new AlbumFolderInfo();
189 | albumFolderInfo.setFolderName(context.getString(R.string.all_image));//设置目录名
190 |
191 | List totalImageInfoList = new ArrayList<>();
192 | albumFolderInfo.setImageInfoList(totalImageInfoList);//设置所有的图片文件
193 |
194 | boolean isFirstAlbum = true; //是否是第一个目录
195 |
196 | Set albumKeySet = albumImageListMap.keySet();
197 | for (String albumKey : albumKeySet) {//每个目录的图片
198 | List albumImageList = albumImageListMap.get(albumKey);
199 |
200 | if (isFirstAlbum == true) {
201 | File frontCover = albumImageList.get(0);
202 | albumFolderInfo.setFrontCover(frontCover);//设置第一张图片
203 |
204 | isFirstAlbum = false;
205 | }
206 | }
207 |
208 | return albumFolderInfo;
209 | } else {
210 | return null;
211 | }
212 | }
213 |
214 |
215 | /**
216 | * 按照文件的修改时间进行排序,越最近修改的,排得越前
217 | */
218 | private void sortByFileLastModified(List files) {
219 | Collections.sort(files, new Comparator() {
220 | @Override
221 | public int compare(File lhs, File rhs) {
222 | if (lhs.lastModified() > rhs.lastModified()) {
223 | return -1;
224 | } else if (lhs.lastModified() < rhs.lastModified()) {
225 | return 1;
226 | }
227 | return 0;
228 | }
229 | });
230 | }
231 |
232 | }
233 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/presenter/ImageScannerPresenter.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.presenter;
2 |
3 | import android.content.Context;
4 | import android.support.v4.app.LoaderManager;
5 |
6 | /**
7 | * 图片扫描Presenter层
8 | *
9 | * Created by Clock on 2016/3/19.
10 | */
11 | public interface ImageScannerPresenter {
12 |
13 | /**
14 | * 扫描获取图片文件夹列表
15 | *
16 | * @param context
17 | * @param loaderManager 获取系统图片的LoaderManager
18 | */
19 | public void startScanImage(Context context, LoaderManager loaderManager);
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/presenter/ImageScannerPresenterImpl.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.presenter;
2 |
3 | import android.content.Context;
4 | import android.support.v4.app.LoaderManager;
5 |
6 | import com.clock.album.presenter.entity.ImageScanResult;
7 | import com.clock.album.model.ImageScannerModel;
8 | import com.clock.album.model.ImageScannerModelImpl;
9 | import com.clock.album.view.AlbumView;
10 | import com.clock.album.view.entity.AlbumViewData;
11 |
12 | /**
13 | * 图片扫描Presenter实现类
14 | *
15 | * Created by Clock on 2016/3/21.
16 | */
17 | public class ImageScannerPresenterImpl implements ImageScannerPresenter {
18 |
19 | private ImageScannerModel mScannerModel;
20 | private AlbumView mAlbumView;
21 |
22 | public ImageScannerPresenterImpl(AlbumView albumView) {
23 | mScannerModel = new ImageScannerModelImpl();
24 | mAlbumView = albumView;
25 | }
26 |
27 | @Override
28 | public void startScanImage(final Context context, LoaderManager loaderManager) {
29 | mScannerModel.startScanImage(context, loaderManager, new ImageScannerModel.OnScanImageFinish() {
30 | @Override
31 | public void onFinish(ImageScanResult imageScanResult) {
32 | if (mAlbumView != null) {
33 | AlbumViewData albumData = mScannerModel.archiveAlbumInfo(context, imageScanResult);
34 | mAlbumView.refreshAlbumData(albumData);
35 | }
36 | }
37 | });
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/presenter/entity/ImageScanResult.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.presenter.entity;
2 |
3 | import java.io.File;
4 | import java.util.ArrayList;
5 | import java.util.List;
6 | import java.util.Map;
7 |
8 | /**
9 | * 扫描图片的结果
10 | *
11 | * Created by Clock on 2016/3/21.
12 | */
13 | public class ImageScanResult {
14 |
15 | /**
16 | * 系统所有有图片的文件夹
17 | */
18 | private List albumFolderList;
19 | /**
20 | * 每个有图片文件夹下面所包含的图片
21 | */
22 | private Map> albumImageListMap;
23 |
24 | /**
25 | * 获取手机上所有有图片的目录
26 | *
27 | * @return
28 | */
29 | public List getAlbumFolderList() {
30 | return albumFolderList;
31 | }
32 |
33 | public void setAlbumFolderList(List albumFolderList) {
34 | this.albumFolderList = albumFolderList;
35 | }
36 |
37 | /**
38 | * 获取手机上所有图片目录下包含的图片
39 | *
40 | * @return 一个Map,key是图片目录路径,value是对应目录下包含的所有图片文件
41 | */
42 | public Map> getAlbumImageListMap() {
43 | return albumImageListMap;
44 | }
45 |
46 | public void setAlbumImageListMap(Map> albumImageListMap) {
47 | this.albumImageListMap = albumImageListMap;
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/ui/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.ui;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.support.v7.app.AppCompatActivity;
6 | import android.view.View;
7 |
8 | import com.clock.album.R;
9 | import com.clock.album.ui.activity.AlbumActivity;
10 |
11 | public class MainActivity extends AppCompatActivity implements View.OnClickListener {
12 |
13 | @Override
14 | protected void onCreate(Bundle savedInstanceState) {
15 | super.onCreate(savedInstanceState);
16 | setContentView(R.layout.activity_main);
17 |
18 | findViewById(R.id.btn_system_album).setOnClickListener(this);
19 | findViewById(R.id.btn_image_loader).setOnClickListener(this);
20 |
21 | }
22 |
23 | @Override
24 | public void onClick(View v) {
25 | int viewId = v.getId();
26 | if (viewId == R.id.btn_system_album) {//系统相册
27 |
28 | Intent albumIntent = new Intent(this, AlbumActivity.class);
29 | startActivity(albumIntent);
30 |
31 | } else if (viewId == R.id.btn_image_loader) {//网络图片加载(各大加载图片框架的实现)
32 |
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/ui/activity/AlbumActivity.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.ui.activity;
2 |
3 | import android.Manifest;
4 | import android.content.DialogInterface;
5 | import android.content.Intent;
6 | import android.content.pm.PackageManager;
7 | import android.net.Uri;
8 | import android.os.Bundle;
9 | import android.provider.Settings;
10 | import android.support.annotation.NonNull;
11 | import android.support.v4.app.ActivityCompat;
12 | import android.support.v4.app.FragmentManager;
13 | import android.support.v4.app.FragmentTransaction;
14 | import android.support.v7.app.AlertDialog;
15 | import android.text.TextUtils;
16 | import android.view.View;
17 | import android.widget.TextView;
18 | import android.widget.Toast;
19 |
20 | import com.clock.album.R;
21 | import com.clock.album.entity.AlbumFolderInfo;
22 | import com.clock.album.entity.ImageInfo;
23 | import com.clock.album.presenter.ImageScannerPresenter;
24 | import com.clock.album.presenter.ImageScannerPresenterImpl;
25 | import com.clock.album.ui.activity.base.BaseActivity;
26 | import com.clock.album.ui.fragment.AlbumDetailFragment;
27 | import com.clock.album.ui.fragment.AlbumFolderFragment;
28 | import com.clock.album.view.AlbumView;
29 | import com.clock.album.view.ImageChooseView;
30 | import com.clock.album.view.entity.AlbumViewData;
31 |
32 | import java.io.File;
33 | import java.util.ArrayList;
34 | import java.util.HashMap;
35 | import java.util.List;
36 |
37 | /**
38 | * 系统相册页面
39 | *
40 | * @author Clock
41 | * @since 2016-01-06
42 | */
43 | public class AlbumActivity extends BaseActivity implements View.OnClickListener, ImageChooseView, AlbumView {
44 |
45 | private final static String TAG = AlbumActivity.class.getSimpleName();
46 | private final static String FRAGMENT_BACK_STACK = "FragmentBackStack";
47 | private final static String PACKAGE_URL_SCHEME = "package:";
48 |
49 | /**
50 | * Android M 的Runtime Permission特性申请权限用的
51 | */
52 | private final static int REQUEST_READ_EXTERNAL_STORAGE_CODE = 1;
53 | /**
54 | * 相册列表页面
55 | */
56 | private AlbumFolderFragment mAlbumFolderFragment;
57 | /**
58 | * 相册详情页面
59 | */
60 | private HashMap mAlbumDetailFragmentMap = new HashMap<>();
61 | /**
62 | * 被选中的图片文件列表
63 | */
64 | private ArrayList mSelectedImageFileList = new ArrayList<>();
65 |
66 | private ImageScannerPresenter mImageScannerPresenter;
67 | /**
68 | * 相册目录信息列表
69 | */
70 | private List mAlbumFolderInfoList;
71 | /**
72 | * 显示图片目录的名称,选中图片的按钮
73 | */
74 | private TextView mTitleView, mSelectedView;
75 |
76 | @Override
77 | protected void onCreate(Bundle savedInstanceState) {
78 | super.onCreate(savedInstanceState);
79 | setContentView(R.layout.activity_album);
80 |
81 | mTitleView = (TextView) findViewById(R.id.tv_dir_title);
82 | mSelectedView = (TextView) findViewById(R.id.tv_selected_ok);
83 | mSelectedView.setOnClickListener(this);
84 |
85 | findViewById(R.id.iv_back).setOnClickListener(this);
86 |
87 | mImageScannerPresenter = new ImageScannerPresenterImpl(this);
88 | if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
89 | if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
90 | Toast.makeText(this, R.string.grant_advice_read_album, Toast.LENGTH_SHORT).show();
91 | }
92 | ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_READ_EXTERNAL_STORAGE_CODE);
93 | } else {
94 | mImageScannerPresenter.startScanImage(getApplicationContext(), getSupportLoaderManager());
95 | }
96 |
97 | }
98 |
99 | /**
100 | * 显示打开权限提示的对话框
101 | */
102 | private void showMissingPermissionDialog() {
103 | AlertDialog.Builder builder = new AlertDialog.Builder(this);
104 | builder.setTitle(R.string.help);
105 | builder.setMessage(R.string.help_content);
106 |
107 | builder.setNegativeButton(R.string.quit, new DialogInterface.OnClickListener() {
108 | @Override
109 | public void onClick(DialogInterface dialog, int which) {
110 | Toast.makeText(AlbumActivity.this, R.string.grant_permission_failure, Toast.LENGTH_SHORT).show();
111 | finish();
112 | }
113 | });
114 |
115 | builder.setPositiveButton(R.string.settings, new DialogInterface.OnClickListener() {
116 | @Override
117 | public void onClick(DialogInterface dialog, int which) {
118 | startSystemSettings();
119 | finish();
120 | }
121 | });
122 |
123 | builder.show();
124 | }
125 |
126 |
127 | /**
128 | * 启动系统权限设置界面
129 | */
130 | private void startSystemSettings() {
131 | Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
132 | intent.setData(Uri.parse(PACKAGE_URL_SCHEME + getPackageName()));
133 | startActivity(intent);
134 | }
135 |
136 | @Override
137 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
138 | super.onRequestPermissionsResult(requestCode, permissions, grantResults);
139 | if (requestCode == REQUEST_READ_EXTERNAL_STORAGE_CODE) {
140 | boolean granted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
141 | if (granted) {
142 | Toast.makeText(this, R.string.grant_permission_success, Toast.LENGTH_SHORT).show();
143 | mImageScannerPresenter.startScanImage(getApplicationContext(), getSupportLoaderManager());
144 |
145 | } else {
146 | showMissingPermissionDialog();//提示对话框
147 | //Toast.makeText(this, R.string.grant_permission_failure, Toast.LENGTH_SHORT).show();
148 | }
149 | }
150 | }
151 |
152 | @Override
153 | public void onClick(View v) {
154 | int viewId = v.getId();
155 | if (viewId == R.id.iv_back) {
156 | onBackPressed();
157 |
158 | } else if (viewId == R.id.tv_selected_ok) {
159 | Intent showSelectedIntent = new Intent(this, ImageSelectActivity.class);
160 | showSelectedIntent.putExtra(ImageSelectActivity.EXTRA_SELECTED_IMAGE_LIST, mSelectedImageFileList);
161 | startActivity(showSelectedIntent);
162 | finish();
163 |
164 | }
165 | }
166 |
167 | @Override
168 | public void switchAlbumFolder(AlbumFolderInfo albumFolderInfo) {
169 | if (albumFolderInfo != null) {
170 | FragmentManager fragmentManager = getSupportFragmentManager();
171 | FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
172 | AlbumDetailFragment albumDetailFragment = mAlbumDetailFragmentMap.get(albumFolderInfo);
173 | if (albumDetailFragment == null) {
174 | List imageInfoList = albumFolderInfo.getImageInfoList();
175 | albumDetailFragment = AlbumDetailFragment.newInstance(imageInfoList);
176 | mAlbumDetailFragmentMap.put(albumFolderInfo, albumDetailFragment);
177 | }
178 | fragmentTransaction.replace(R.id.fragment_container, albumDetailFragment);
179 | fragmentTransaction.addToBackStack(FRAGMENT_BACK_STACK);
180 | fragmentTransaction.commit();
181 |
182 | refreshFolderName(albumFolderInfo.getFolderName());
183 | }
184 | }
185 |
186 | /**
187 | * 刷新目录名称
188 | *
189 | * @param albumFolderName
190 | */
191 | private void refreshFolderName(String albumFolderName) {
192 | if (!TextUtils.isEmpty(albumFolderName)) {
193 | mTitleView.setText(albumFolderName);
194 | }
195 | }
196 |
197 | /**
198 | * 切换到相册列表
199 | */
200 | private void switchAlbumFolderList() {
201 | final FragmentManager fragmentManager = getSupportFragmentManager();
202 | FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
203 | fragmentTransaction.replace(R.id.fragment_container, mAlbumFolderFragment);
204 | fragmentManager.addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
205 | @Override
206 | public void onBackStackChanged() {
207 | int backStackCount = fragmentManager.getBackStackEntryCount();
208 | if (backStackCount == 0) {
209 | AlbumFolderInfo albumFolderInfo = mAlbumFolderInfoList.get(0);
210 | String folderName = albumFolderInfo.getFolderName();
211 | refreshFolderName(folderName);
212 | }
213 | }
214 | });
215 | //fragmentTransaction.commit(); //会产生 java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
216 | fragmentTransaction.commitAllowingStateLoss();//http://stackoverflow.com/questions/25486656/java-lang-illegalstateexceptioncan-not-perform-this-action-after-onsaveinstance
217 | }
218 |
219 |
220 | /**
221 | * 刷新选中按钮的状态
222 | */
223 | private void refreshSelectedViewState() {
224 | if (mSelectedImageFileList.size() == 0) {
225 | mSelectedView.setVisibility(View.GONE);
226 |
227 | } else {
228 | String selectedStringFormat = getString(R.string.selected_ok);
229 | int selectedSize = mSelectedImageFileList.size();
230 | AlbumFolderInfo albumFolderInfo = mAlbumFolderInfoList.get(0);
231 | int totalSize = albumFolderInfo.getImageInfoList().size();
232 | String selectedString = String.format(selectedStringFormat, selectedSize, totalSize);
233 | mSelectedView.setText(selectedString);
234 | mSelectedView.setVisibility(View.VISIBLE);
235 |
236 | }
237 | }
238 |
239 | @Override
240 | public void refreshAlbumData(AlbumViewData albumData) {
241 | if (albumData != null) {
242 | mAlbumFolderInfoList = albumData.getAlbumFolderInfoList();
243 | mAlbumFolderFragment = AlbumFolderFragment.newInstance(mAlbumFolderInfoList);
244 | switchAlbumFolderList();
245 |
246 | findViewById(R.id.fragment_container).setVisibility(View.VISIBLE);//显示相册列表区域
247 |
248 | } else {
249 | findViewById(R.id.fragment_container).setVisibility(View.GONE);//隐藏显示相册列表的区域
250 | findViewById(R.id.tv_no_image).setVisibility(View.VISIBLE);//显示没有相片的提示
251 |
252 | }
253 | }
254 |
255 | @Override
256 | public void refreshSelectedCounter(ImageInfo imageInfo) {
257 | if (imageInfo != null) {
258 | boolean isSelected = imageInfo.isSelected();
259 | File imageFile = imageInfo.getImageFile();
260 | if (isSelected) {//选中
261 | if (!mSelectedImageFileList.contains(imageFile)) {
262 | mSelectedImageFileList.add(imageFile);
263 | }
264 | } else {//取消选中
265 | if (mSelectedImageFileList.contains(imageFile)) {
266 | mSelectedImageFileList.remove(imageFile);
267 | }
268 | }
269 | refreshSelectedViewState();
270 | }
271 | }
272 |
273 | }
274 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/ui/activity/ImagePreviewActivity.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.ui.activity;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.os.Build;
6 | import android.os.Bundle;
7 | import android.support.v4.view.PagerAdapter;
8 | import android.support.v4.view.ViewPager;
9 | import android.util.Log;
10 | import android.view.View;
11 | import android.view.ViewGroup;
12 | import android.view.animation.AnimationUtils;
13 | import android.widget.CheckBox;
14 | import android.widget.CompoundButton;
15 | import android.widget.TextView;
16 |
17 | import com.clock.album.R;
18 | import com.clock.album.entity.ImageInfo;
19 | import com.clock.album.imageloader.ImageLoaderFactory;
20 | import com.clock.album.imageloader.ImageLoaderWrapper;
21 | import com.clock.album.ui.activity.base.BaseActivity;
22 |
23 | import java.io.Serializable;
24 | import java.util.List;
25 |
26 | import uk.co.senab.photoview.PhotoView;
27 | import uk.co.senab.photoview.PhotoViewAttacher;
28 |
29 | /**
30 | * 图片预览界面
31 | *
32 | * @author Clock
33 | * @since 2016-01-25
34 | */
35 | public class ImagePreviewActivity extends BaseActivity implements View.OnClickListener, CompoundButton.OnCheckedChangeListener {
36 |
37 | private final static String TAG = ImagePreviewActivity.class.getSimpleName();
38 |
39 | public final static String EXTRA_IMAGE_INFO_LIST = "ImageInfoList";
40 | public final static String EXTRA_IMAGE_INFO = "ImageInfo";
41 |
42 | public final static String EXTRA_NEW_IMAGE_LIST = "NewImageList";
43 |
44 | private ViewPager mPreviewViewPager;
45 | private PagerAdapter mPreviewPagerAdapter;
46 | private ViewPager.OnPageChangeListener mPreviewChangeListener;
47 | private TextView mTitleView;
48 | private CheckBox mImageSelectedBox;
49 | private View mHeaderView, mFooterView;
50 |
51 | /**
52 | * 所有图片的列表
53 | */
54 | private List mPreviewImageInfoList;
55 | /**
56 | * 刚进入页面显示的图片
57 | */
58 | private ImageInfo mPreviewImageInfo;
59 |
60 | private ImageLoaderWrapper mImageLoaderWrapper;
61 |
62 | @Override
63 | protected void onCreate(Bundle savedInstanceState) {
64 | super.onCreate(savedInstanceState);
65 | setContentView(R.layout.activity_image_preview);
66 |
67 | if (Build.VERSION.SDK_INT >= 11) {
68 | getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
69 | @Override
70 | public void onSystemUiVisibilityChange(int visibility) {
71 | if (View.SYSTEM_UI_FLAG_VISIBLE == visibility) {//此处需要添加顶部和底部消失和出现的动画效果
72 | Log.i(TAG, "SYSTEM_UI_FLAG_VISIBLE");
73 | mHeaderView.startAnimation(AnimationUtils.loadAnimation(ImagePreviewActivity.this, R.anim.top_enter_anim));
74 | mFooterView.startAnimation(AnimationUtils.loadAnimation(ImagePreviewActivity.this, R.anim.bottom_enter_anim));
75 |
76 | } else {
77 | Log.i(TAG, "SYSTEM_UI_FLAG_INVISIBLE");
78 | mHeaderView.startAnimation(AnimationUtils.loadAnimation(ImagePreviewActivity.this, R.anim.top_exit_anim));
79 | mFooterView.startAnimation(AnimationUtils.loadAnimation(ImagePreviewActivity.this, R.anim.bottom_exit_anim));
80 |
81 | }
82 | }
83 | });
84 | }
85 |
86 | mImageLoaderWrapper = ImageLoaderFactory.getLoader();
87 |
88 | mPreviewImageInfo = (ImageInfo) getIntent().getSerializableExtra(EXTRA_IMAGE_INFO);
89 | mPreviewImageInfoList = (List) getIntent().getSerializableExtra(EXTRA_IMAGE_INFO_LIST);
90 |
91 | initView();
92 |
93 | }
94 |
95 | private void initView() {
96 |
97 | mTitleView = (TextView) findViewById(R.id.tv_title);
98 | if (mPreviewImageInfo != null && mPreviewImageInfoList != null) {
99 | if (mPreviewImageInfoList.contains(mPreviewImageInfo)) {
100 | int imageIndex = mPreviewImageInfoList.indexOf(mPreviewImageInfo);
101 | setPositionToTitle(imageIndex);
102 |
103 | }
104 | }
105 |
106 | mImageSelectedBox = (CheckBox) findViewById(R.id.ckb_image_select);
107 | if (mPreviewImageInfo != null) {
108 | mImageSelectedBox.setChecked(mPreviewImageInfo.isSelected());
109 | }
110 | mImageSelectedBox.setOnCheckedChangeListener(this);
111 |
112 | mPreviewViewPager = (ViewPager) findViewById(R.id.gallery_viewpager);
113 | mPreviewPagerAdapter = new PreviewPagerAdapter();
114 | mPreviewViewPager.setAdapter(mPreviewPagerAdapter);
115 | if (mPreviewImageInfo != null && mPreviewImageInfoList != null && mPreviewImageInfoList.contains(mPreviewImageInfo)) {
116 | int initShowPosition = mPreviewImageInfoList.indexOf(mPreviewImageInfo);
117 | mPreviewViewPager.setCurrentItem(initShowPosition);
118 | }
119 | mPreviewChangeListener = new PreviewChangeListener();
120 | mPreviewViewPager.addOnPageChangeListener(mPreviewChangeListener);
121 |
122 | findViewById(R.id.iv_back).setOnClickListener(this);
123 |
124 | mHeaderView = findViewById(R.id.header_view);
125 | mFooterView = findViewById(R.id.footer_view);
126 | }
127 |
128 | @Override
129 | public void onClick(View v) {
130 | int viewId = v.getId();
131 | if (viewId == R.id.iv_back) {
132 | onBackPressed();
133 |
134 | }
135 | }
136 |
137 | @Override
138 | public void onBackPressed() {
139 | Intent data = new Intent();
140 | data.putExtra(EXTRA_NEW_IMAGE_LIST, (Serializable) mPreviewImageInfoList);
141 | setResult(Activity.RESULT_OK, data);
142 | super.onBackPressed();
143 | }
144 |
145 | @Override
146 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
147 | if (buttonView == mImageSelectedBox) {
148 | int currentPosition = mPreviewViewPager.getCurrentItem();
149 | ImageInfo imageInfo = mPreviewImageInfoList.get(currentPosition);
150 | imageInfo.setIsSelected(isChecked);
151 | }
152 | }
153 |
154 | /**
155 | * 监听PhotoView的点击事件
156 | */
157 | private PhotoViewAttacher.OnViewTapListener mOnPreviewTapListener = new PhotoViewAttacher.OnViewTapListener() {
158 | @Override
159 | public void onViewTap(View view, float v, float v1) {
160 | toggleImmersiveMode();
161 | }
162 | };
163 |
164 | /**
165 | * 相册适配器
166 | */
167 | private class PreviewPagerAdapter extends PagerAdapter {
168 |
169 | @Override
170 | public int getCount() {
171 | if (mPreviewImageInfoList == null) {
172 | return 0;
173 | }
174 | return mPreviewImageInfoList.size();
175 | }
176 |
177 | @Override
178 | public boolean isViewFromObject(View view, Object object) {
179 | PhotoView galleryPhotoView = (PhotoView) view.findViewById(R.id.iv_show_image);
180 | galleryPhotoView.setScale(1.0f);//让图片在滑动过程中恢复回缩放操作前原图大小
181 | return view == object;
182 | }
183 |
184 | @Override
185 | public Object instantiateItem(ViewGroup container, int position) {
186 | View galleryItemView = View.inflate(ImagePreviewActivity.this, R.layout.preview_image_item, null);
187 |
188 | ImageInfo imageInfo = mPreviewImageInfoList.get(position);
189 | PhotoView galleryPhotoView = (PhotoView) galleryItemView.findViewById(R.id.iv_show_image);
190 | galleryPhotoView.setOnViewTapListener(mOnPreviewTapListener);
191 | ImageLoaderWrapper.DisplayOption displayOption = new ImageLoaderWrapper.DisplayOption();
192 | displayOption.loadErrorResId = R.mipmap.img_error;
193 | displayOption.loadingResId = R.mipmap.img_default;
194 | mImageLoaderWrapper.displayImage(galleryPhotoView, imageInfo.getImageFile(), displayOption);
195 |
196 | container.addView(galleryItemView);
197 | return galleryItemView;
198 | }
199 |
200 | @Override
201 | public void destroyItem(ViewGroup container, int position, Object object) {
202 | container.removeView((View) object);
203 | }
204 |
205 | }
206 |
207 | /**
208 | * 相册详情页面滑动监听
209 | */
210 | private class PreviewChangeListener implements ViewPager.OnPageChangeListener {
211 |
212 | @Override
213 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
214 |
215 | }
216 |
217 | @Override
218 | public void onPageSelected(int position) {
219 | mImageSelectedBox.setOnCheckedChangeListener(null);//先反注册监听,避免重复更新选中的状态
220 |
221 | setPositionToTitle(position);
222 | ImageInfo imageInfo = mPreviewImageInfoList.get(position);
223 | mImageSelectedBox.setChecked(imageInfo.isSelected());
224 |
225 | mImageSelectedBox.setOnCheckedChangeListener(ImagePreviewActivity.this);
226 | }
227 |
228 | @Override
229 | public void onPageScrollStateChanged(int state) {
230 |
231 | }
232 | }
233 |
234 | /**
235 | * 设置标题现实当前所处的位置
236 | *
237 | * @param position
238 | */
239 | private void setPositionToTitle(int position) {
240 | if (mPreviewImageInfoList != null) {
241 | String title = String.format(getString(R.string.image_index), position + 1, mPreviewImageInfoList.size());
242 | mTitleView.setText(title);
243 | }
244 | }
245 |
246 | /**
247 | * 切换沉浸栏模式(Immersive - Mode)
248 | */
249 | private void toggleImmersiveMode() {
250 | if (Build.VERSION.SDK_INT >= 11) {
251 | int uiOptions = getWindow().getDecorView().getSystemUiVisibility();
252 | // Navigation bar hiding: Backwards compatible to ICS.
253 | if (Build.VERSION.SDK_INT >= 14) {
254 | uiOptions ^= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
255 | }
256 | // Status bar hiding: Backwards compatible to Jellybean
257 | if (Build.VERSION.SDK_INT >= 16) {
258 | uiOptions ^= View.SYSTEM_UI_FLAG_FULLSCREEN;
259 | }
260 | // Immersive mode: Backward compatible to KitKat.
261 | if (Build.VERSION.SDK_INT >= 18) {
262 | uiOptions ^= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
263 | }
264 | getWindow().getDecorView().setSystemUiVisibility(uiOptions);
265 | }
266 | }
267 | }
268 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/ui/activity/ImageSelectActivity.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.ui.activity;
2 |
3 | import android.os.Bundle;
4 | import android.view.View;
5 | import android.view.ViewGroup;
6 | import android.widget.AbsListView;
7 | import android.widget.BaseAdapter;
8 | import android.widget.GridView;
9 | import android.widget.ImageView;
10 |
11 | import com.clock.album.R;
12 | import com.clock.album.imageloader.ImageLoaderFactory;
13 | import com.clock.album.imageloader.ImageLoaderWrapper;
14 | import com.clock.album.ui.activity.base.BaseActivity;
15 | import com.clock.utils.common.RuleUtils;
16 |
17 | import java.io.File;
18 | import java.util.List;
19 |
20 | /**
21 | * 显示选中图片的界面
22 | *
23 | * @author Clock
24 | * @since 2016-01-26
25 | */
26 | public class ImageSelectActivity extends BaseActivity implements View.OnClickListener {
27 |
28 | public final static String EXTRA_SELECTED_IMAGE_LIST = "selectImage";
29 |
30 | private GridView mSelectedImageGridView;
31 | private List mSelectedImageList;
32 | private ImageLoaderWrapper mImageLoaderWrapper;
33 |
34 | @Override
35 | protected void onCreate(Bundle savedInstanceState) {
36 | super.onCreate(savedInstanceState);
37 | setContentView(R.layout.activity_image_select);
38 |
39 | findViewById(R.id.iv_back).setOnClickListener(this);
40 |
41 | mImageLoaderWrapper = ImageLoaderFactory.getLoader();
42 | mSelectedImageList = (List) getIntent().getSerializableExtra(EXTRA_SELECTED_IMAGE_LIST);
43 |
44 | mSelectedImageGridView = (GridView) findViewById(R.id.gv_image_selected);
45 | mSelectedImageGridView.setAdapter(new SelectedImageGridAdapter());
46 |
47 | }
48 |
49 | @Override
50 | public void onClick(View v) {
51 | int viewId = v.getId();
52 | if (viewId == R.id.iv_back) {
53 | onBackPressed();
54 |
55 | }
56 | }
57 |
58 | private class SelectedImageGridAdapter extends BaseAdapter {
59 |
60 | @Override
61 | public int getCount() {
62 | if (mSelectedImageList == null) {
63 | return 0;
64 | }
65 | return mSelectedImageList.size();
66 | }
67 |
68 | @Override
69 | public Object getItem(int position) {
70 | return mSelectedImageList.get(position);
71 | }
72 |
73 | @Override
74 | public long getItemId(int position) {
75 | return position;
76 | }
77 |
78 | @Override
79 | public View getView(int position, View convertView, ViewGroup parent) {
80 | SelectedImageHolder holder = null;
81 | if (convertView == null) {
82 | holder = new SelectedImageHolder();
83 | convertView = View.inflate(parent.getContext(), R.layout.selected_image_item, null);
84 |
85 | int gridItemSpacing = (int) RuleUtils.convertDp2Px(parent.getContext(), 2);
86 | int gridEdgeLength = (RuleUtils.getScreenWidth(parent.getContext()) - gridItemSpacing * 2) / 3;
87 |
88 | AbsListView.LayoutParams layoutParams = new AbsListView.LayoutParams(gridEdgeLength, gridEdgeLength);
89 | convertView.setLayoutParams(layoutParams);
90 | holder.selectedImageView = (ImageView) convertView.findViewById(R.id.iv_selected_item);
91 | convertView.setTag(holder);
92 |
93 | } else {
94 | holder = (SelectedImageHolder) convertView.getTag();
95 |
96 | }
97 |
98 | ImageLoaderWrapper.DisplayOption displayOption = new ImageLoaderWrapper.DisplayOption();
99 | displayOption.loadingResId = R.mipmap.img_default;
100 | displayOption.loadErrorResId = R.mipmap.img_error;
101 | mImageLoaderWrapper.displayImage(holder.selectedImageView, mSelectedImageList.get(position), displayOption);
102 |
103 | return convertView;
104 | }
105 | }
106 |
107 | private static class SelectedImageHolder {
108 | ImageView selectedImageView;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/ui/activity/base/BaseActivity.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.ui.activity.base;
2 |
3 | import android.os.Bundle;
4 | import android.support.v7.app.AppCompatActivity;
5 | import android.view.Window;
6 |
7 | /**
8 | * 所有Activity方法的基类
9 | *
10 | * Created by Clock on 2016/1/14.
11 | */
12 | public class BaseActivity extends AppCompatActivity {
13 |
14 | @Override
15 | protected void onCreate(Bundle savedInstanceState) {
16 | supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
17 | super.onCreate(savedInstanceState);
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/ui/fragment/AlbumDetailFragment.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.ui.fragment;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.os.Bundle;
7 | import android.view.LayoutInflater;
8 | import android.view.View;
9 | import android.view.ViewGroup;
10 | import android.widget.BaseAdapter;
11 | import android.widget.GridView;
12 |
13 | import com.clock.album.R;
14 | import com.clock.album.adapter.AlbumGridAdapter;
15 | import com.clock.album.entity.ImageInfo;
16 | import com.clock.album.imageloader.ImageLoaderFactory;
17 | import com.clock.album.imageloader.ImageLoaderWrapper;
18 | import com.clock.album.ui.activity.ImagePreviewActivity;
19 | import com.clock.album.ui.fragment.base.BaseFragment;
20 | import com.clock.album.view.ImageChooseView;
21 |
22 | import java.io.Serializable;
23 | import java.util.List;
24 |
25 | /**
26 | * 相册详情页面
27 | *
28 | * @author Clock
29 | * @since 2016-01-17
30 | */
31 | public class AlbumDetailFragment extends BaseFragment implements AlbumGridAdapter.OnClickPreviewImageListener {
32 |
33 | public static final int PREVIEW_REQUEST_CODE = 1000;
34 |
35 | private static final String ARG_PARAM1 = "param1";
36 |
37 | /**
38 | * 图片选择View层交互接口
39 | */
40 | private ImageChooseView mImageChooseView;
41 | /**
42 | * 相册信息列表
43 | */
44 | private List mImageInfoList;
45 | /**
46 | * 相册视图控件
47 | */
48 | private GridView mAlbumGridView;
49 | private BaseAdapter mAlbumGridViewAdapter;
50 |
51 | /**
52 | * @param imageInfoList 相册列表
53 | * @return
54 | */
55 | public static AlbumDetailFragment newInstance(List imageInfoList) {
56 | AlbumDetailFragment fragment = new AlbumDetailFragment();
57 | Bundle args = new Bundle();
58 | args.putSerializable(ARG_PARAM1, (Serializable) imageInfoList);
59 | fragment.setArguments(args);
60 | return fragment;
61 | }
62 |
63 | public AlbumDetailFragment() {
64 | // Required empty public constructor
65 | }
66 |
67 | @Override
68 | public void onCreate(Bundle savedInstanceState) {
69 | super.onCreate(savedInstanceState);
70 | if (getArguments() != null) {
71 | mImageInfoList = (List) getArguments().getSerializable(ARG_PARAM1);
72 | }
73 | }
74 |
75 | @Override
76 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
77 | // Inflate the layout for this fragment
78 | View rootView = inflater.inflate(R.layout.fragment_album_detail, container, false);
79 | mAlbumGridView = (GridView) rootView.findViewById(R.id.gv_album);
80 | ImageLoaderWrapper loaderWrapper = ImageLoaderFactory.getLoader();
81 | mAlbumGridViewAdapter = new AlbumGridAdapter(mImageInfoList, loaderWrapper, mImageChooseView, this);
82 | mAlbumGridView.setAdapter(mAlbumGridViewAdapter);
83 | return rootView;
84 | }
85 |
86 | @Override
87 | public void onAttach(Context context) {
88 | super.onAttach(context);
89 | if (context instanceof ImageChooseView) {
90 | mImageChooseView = (ImageChooseView) context;
91 | }
92 | }
93 |
94 | @Override
95 | public void onDetach() {
96 | super.onDetach();
97 | mImageChooseView = null;
98 | }
99 |
100 | @Override
101 | public void onActivityResult(int requestCode, int resultCode, Intent data) {
102 | if (requestCode == PREVIEW_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
103 | List newSelectedImageList = (List) data.getSerializableExtra(ImagePreviewActivity.EXTRA_NEW_IMAGE_LIST);
104 | refreshSelectedImage(newSelectedImageList);
105 |
106 | } else {
107 | super.onActivityResult(requestCode, resultCode, data);
108 | }
109 | }
110 |
111 | /**
112 | * 刷新新选中图片的数据
113 | *
114 | * @param newSelectedImageList
115 | */
116 | private void refreshSelectedImage(List newSelectedImageList) {
117 | int imageSize = newSelectedImageList.size();
118 | for (int imagePos = 0; imagePos < imageSize; imagePos++) {
119 | ImageInfo srcImageInfo = newSelectedImageList.get(imagePos);
120 | ImageInfo destImageInfo = mImageInfoList.get(imagePos);
121 | destImageInfo.setIsSelected(srcImageInfo.isSelected());//遍历更新选中的状态
122 | if (mImageChooseView != null) {
123 | mImageChooseView.refreshSelectedCounter(destImageInfo);
124 | }
125 | }
126 | mAlbumGridViewAdapter.notifyDataSetChanged();
127 | }
128 |
129 | @Override
130 | public void onClickPreview(ImageInfo imageInfo) {
131 | Intent previewIntent = new Intent(getContext(), ImagePreviewActivity.class);
132 | previewIntent.putExtra(ImagePreviewActivity.EXTRA_IMAGE_INFO, imageInfo);
133 | previewIntent.putExtra(ImagePreviewActivity.EXTRA_IMAGE_INFO_LIST, (Serializable) mImageInfoList);
134 | startActivityForResult(previewIntent, PREVIEW_REQUEST_CODE);
135 | }
136 |
137 | }
138 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/ui/fragment/AlbumFolderFragment.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.ui.fragment;
2 |
3 | import android.content.Context;
4 | import android.os.Bundle;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 | import android.widget.AdapterView;
9 | import android.widget.ListView;
10 |
11 | import com.clock.album.R;
12 | import com.clock.album.adapter.AlbumFolderAdapter;
13 | import com.clock.album.entity.AlbumFolderInfo;
14 | import com.clock.album.imageloader.ImageLoaderFactory;
15 | import com.clock.album.imageloader.ImageLoaderWrapper;
16 | import com.clock.album.ui.fragment.base.BaseFragment;
17 | import com.clock.album.view.AlbumView;
18 |
19 | import java.io.Serializable;
20 | import java.util.List;
21 |
22 | /**
23 | * 相册目录页面
24 | *
25 | * @author Clock
26 | * @since 2016-01-17
27 | */
28 | public class AlbumFolderFragment extends BaseFragment implements AdapterView.OnItemClickListener {
29 |
30 | private static final String ARG_PARAM1 = "param1";
31 |
32 | private AlbumView mAlbumView;
33 | /**
34 | * 相册目录列表
35 | */
36 | private List mAlbumFolderInfoList;
37 | private ListView mFolderListView;
38 |
39 | public AlbumFolderFragment() {
40 | // Required empty public constructor
41 | }
42 |
43 | /**
44 | * @param albumFolderInfoList 相册目录列表
45 | * @return
46 | */
47 | public static AlbumFolderFragment newInstance(List albumFolderInfoList) {
48 | AlbumFolderFragment fragment = new AlbumFolderFragment();
49 | Bundle args = new Bundle();
50 | args.putSerializable(ARG_PARAM1, (Serializable) albumFolderInfoList);
51 | fragment.setArguments(args);
52 | return fragment;
53 | }
54 |
55 | @Override
56 | public void onCreate(Bundle savedInstanceState) {
57 | super.onCreate(savedInstanceState);
58 | if (getArguments() != null) {
59 | mAlbumFolderInfoList = (List) getArguments().getSerializable(ARG_PARAM1);
60 | }
61 | }
62 |
63 | @Override
64 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
65 | // Inflate the layout for this fragment
66 | View rootView = inflater.inflate(R.layout.fragment_album_directory, container, false);
67 | mFolderListView = (ListView) rootView.findViewById(R.id.list_album);
68 | ImageLoaderWrapper loaderWrapper = ImageLoaderFactory.getLoader();
69 | AlbumFolderAdapter albumFolderAdapter = new AlbumFolderAdapter(mAlbumFolderInfoList, loaderWrapper);
70 | mFolderListView.setAdapter(albumFolderAdapter);
71 | mFolderListView.setOnItemClickListener(this);
72 | return rootView;
73 | }
74 |
75 |
76 | @Override
77 | public void onAttach(Context context) {
78 | super.onAttach(context);
79 | if (context instanceof AlbumView) {
80 | mAlbumView = (AlbumView) context;
81 | }
82 | }
83 |
84 | @Override
85 | public void onDetach() {
86 | super.onDetach();
87 | mAlbumView = null;
88 | }
89 |
90 | @Override
91 | public void onItemClick(AdapterView> parent, View view, int position, long id) {
92 | if (parent == mFolderListView) {
93 | if (mAlbumView != null) {
94 | AlbumFolderInfo albumFolderInfo = mAlbumFolderInfoList.get(position);
95 | mAlbumView.switchAlbumFolder(albumFolderInfo);
96 | }
97 | }
98 | }
99 |
100 | }
101 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/ui/fragment/base/BaseFragment.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.ui.fragment.base;
2 |
3 | import android.support.v4.app.Fragment;
4 |
5 | /**
6 | * Created by Clock on 2016/1/27.
7 | */
8 | public class BaseFragment extends Fragment {
9 |
10 | @Override
11 | public void onResume() {
12 | super.onResume();
13 | }
14 |
15 | @Override
16 | public void onPause() {
17 | super.onPause();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/ui/widget/HackyViewPager.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.ui.widget;
2 |
3 | import android.content.Context;
4 | import android.support.v4.view.ViewPager;
5 | import android.util.AttributeSet;
6 | import android.view.MotionEvent;
7 |
8 | /**
9 | * Found at http://stackoverflow.com/questions/7814017/is-it-possible-to-disable-scrolling-on-a-viewpager.
10 | * Convenient way to temporarily disable ViewPager navigation while interacting with ImageView.
11 | *
12 | * Julia Zudikova
13 | */
14 |
15 | /**
16 | * Hacky fix for Issue #4 and
17 | * http://code.google.com/p/android/issues/detail?id=18990
18 | *
19 | * ScaleGestureDetector seems to mess up the touch events, which means that
20 | * ViewGroups which make use of onInterceptTouchEvent throw a lot of
21 | * IllegalArgumentException: pointerIndex out of range.
22 | *
23 | * There's not much I can do in my code for now, but we can mask the result by
24 | * just catching the problem and ignoring it.
25 | *
26 | * @author Chris Banes
27 | */
28 | public class HackyViewPager extends ViewPager {
29 |
30 | private boolean isLocked;
31 |
32 | public HackyViewPager(Context context) {
33 | super(context);
34 | isLocked = false;
35 | }
36 |
37 | public HackyViewPager(Context context, AttributeSet attrs) {
38 | super(context, attrs);
39 | isLocked = false;
40 | }
41 |
42 | @Override
43 | public boolean onInterceptTouchEvent(MotionEvent ev) {
44 | if (!isLocked) {
45 | try {
46 | return super.onInterceptTouchEvent(ev);
47 | } catch (IllegalArgumentException e) {
48 | e.printStackTrace();
49 | return false;
50 | }
51 | }
52 | return false;
53 | }
54 |
55 | @Override
56 | public boolean onTouchEvent(MotionEvent event) {
57 | return !isLocked && super.onTouchEvent(event);
58 | }
59 |
60 | public void toggleLock() {
61 | isLocked = !isLocked;
62 | }
63 |
64 | public void setLocked(boolean isLocked) {
65 | this.isLocked = isLocked;
66 | }
67 |
68 | public boolean isLocked() {
69 | return isLocked;
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/view/AlbumView.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.view;
2 |
3 | import com.clock.album.entity.AlbumFolderInfo;
4 | import com.clock.album.view.entity.AlbumViewData;
5 |
6 | /**
7 | * Created by Clock on 2016/3/19.
8 | */
9 | public interface AlbumView {
10 |
11 | /**
12 | * 刷新相册数据信息
13 | *
14 | * @param albumData
15 | */
16 | public void refreshAlbumData(AlbumViewData albumData);
17 |
18 | /**
19 | * 切换图片目录
20 | *
21 | * @param albumFolderInfo 指定图片目录的信息
22 | */
23 | public void switchAlbumFolder(AlbumFolderInfo albumFolderInfo);
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/view/ImageChooseView.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.view;
2 |
3 | import com.clock.album.entity.ImageInfo;
4 |
5 | import java.io.File;
6 |
7 | /**
8 | * 图片选择器View层接口
9 | *
10 | * Created by Clock on 2016/3/21.
11 | */
12 | public interface ImageChooseView {
13 |
14 | /**
15 | * 刷新图片的计数器
16 | *
17 | * @param imageInfo 进行操作的文件信息
18 | */
19 | public void refreshSelectedCounter(ImageInfo imageInfo);
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/clock/album/view/entity/AlbumViewData.java:
--------------------------------------------------------------------------------
1 | package com.clock.album.view.entity;
2 |
3 | import com.clock.album.entity.AlbumFolderInfo;
4 |
5 | import java.util.List;
6 |
7 | /**
8 | * 相册界面需要的数据
9 | *
10 | * Created by Clock on 2016/3/21.
11 | */
12 | public class AlbumViewData {
13 |
14 | /**
15 | * 图片总数
16 | */
17 | //private int imageTotal;
18 | /**
19 | * 所有图片的信息列表(图片目录的绝对路径作为map的key,value是该图片目录下的所有图片文件信息)
20 | */
21 | //private Map> albumImageInfoListMap;
22 | /**
23 | * 所有有图片的目录信息
24 | */
25 | //private List albumInfoList;
26 | /**
27 | * 相册目录列表
28 | */
29 | private List albumFolderInfoList;
30 |
31 | /*public Map> getAlbumImageInfoListMap() {
32 | return albumImageInfoListMap;
33 | }
34 |
35 | public void setAlbumImageInfoListMap(Map> albumImageInfoListMap) {
36 | this.albumImageInfoListMap = albumImageInfoListMap;
37 | }
38 |
39 | public List getAlbumInfoList() {
40 | return albumInfoList;
41 | }
42 |
43 | public void setAlbumInfoList(List albumInfoList) {
44 | this.albumInfoList = albumInfoList;
45 | }
46 |
47 | public int getImageTotal() {
48 | return imageTotal;
49 | }
50 |
51 | public void setImageTotal(int imageTotal) {
52 | this.imageTotal = imageTotal;
53 | }*/
54 |
55 | public List getAlbumFolderInfoList() {
56 | return albumFolderInfoList;
57 | }
58 |
59 | public void setAlbumFolderInfoList(List albumFolderInfoList) {
60 | this.albumFolderInfoList = albumFolderInfoList;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/bottom_enter_anim.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/bottom_exit_anim.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/top_enter_anim.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/top_exit_anim.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/image_selector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_album.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
21 |
22 |
34 |
35 |
46 |
47 |
48 |
53 |
54 |
61 |
62 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_image_preview.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
11 |
16 |
17 |
24 |
25 |
33 |
34 |
35 |
36 |
42 |
43 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_image_select.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
20 |
21 |
30 |
31 |
32 |
33 |
42 |
43 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
16 |
17 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/album_directory_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
21 |
22 |
28 |
29 |
34 |
35 |
36 |
37 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/album_grid_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
12 |
13 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_album_detail.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_album_directory.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/preview_image_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/selected_image_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/group_item_arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/D-clock/AndroidAlbum/27fc725451e384fee2c383c2ca0debbf9675ea61/app/src/main/res/mipmap-hdpi/group_item_arrow.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/D-clock/AndroidAlbum/27fc725451e384fee2c383c2ca0debbf9675ea61/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/img_default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/D-clock/AndroidAlbum/27fc725451e384fee2c383c2ca0debbf9675ea61/app/src/main/res/mipmap-hdpi/img_default.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/img_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/D-clock/AndroidAlbum/27fc725451e384fee2c383c2ca0debbf9675ea61/app/src/main/res/mipmap-hdpi/img_error.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/navi_back_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/D-clock/AndroidAlbum/27fc725451e384fee2c383c2ca0debbf9675ea61/app/src/main/res/mipmap-hdpi/navi_back_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/D-clock/AndroidAlbum/27fc725451e384fee2c383c2ca0debbf9675ea61/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/D-clock/AndroidAlbum/27fc725451e384fee2c383c2ca0debbf9675ea61/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/image_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/D-clock/AndroidAlbum/27fc725451e384fee2c383c2ca0debbf9675ea61/app/src/main/res/mipmap-xhdpi/image_selected.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/image_unselected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/D-clock/AndroidAlbum/27fc725451e384fee2c383c2ca0debbf9675ea61/app/src/main/res/mipmap-xhdpi/image_unselected.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/D-clock/AndroidAlbum/27fc725451e384fee2c383c2ca0debbf9675ea61/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/D-clock/AndroidAlbum/27fc725451e384fee2c383c2ca0debbf9675ea61/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-v19/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | AndroidAlbum
3 | 本地图片选择器
4 | 网络图片加载
5 | 全部图片
6 | 选中的图片
7 | 确定 (%1$s/%2$s)
8 | 选中
9 | %1$s/%2$s
10 | 授权成功,正在读取相册
11 | 授权失败,无法读取相册
12 | 建议授予获取相册的权限
13 | 系统权限
14 | 缺少读取系统相册的权限,请点击 "设置" - "权限" 授予应用权限
15 | 退出
16 | 设置
17 | 没有相册图片
18 | 调用系统相机拍照
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/test/java/com/clock/album/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.clock.album;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:2.2.2'
9 | classpath 'com.tencent.bugly:symtabfileuploader:latest.release'
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | jcenter()
18 | }
19 | }
20 |
21 | task clean(type: Delete) {
22 | delete rootProject.buildDir
23 | }
24 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 | android.useDeprecatedNdk=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/D-clock/AndroidAlbum/27fc725451e384fee2c383c2ca0debbf9675ea61/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Dec 28 10:00:20 PST 2015
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------