├── .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 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | ![本地图片选择功能](http://f.hiphotos.baidu.com/image/pic/item/ae51f3deb48f8c54c954df5f3d292df5e0fe7f3e.jpg) 26 | 27 | - 图片详情预览页面,添加选图功能和沉浸模式(Immersive-Mode)效果 28 | 29 | ![选图功能和沉浸模式效果](http://b.hiphotos.baidu.com/image/pic/item/838ba61ea8d3fd1f3071ac4c374e251f95ca5f4f.jpg) 30 | 31 | ## 闪退日志处理 32 | 33 | **1.本地闪退日志处理** 34 | 35 | > 本地化存储闪退日志信息除了闪退的log外,还包含:设备厂商,设备名称,系统版本号,app版本号,设备id(IMEI)等。发生闪退后可以通过文件浏览器在SD卡上找到报错的log信息。(目前闪退日志是存放到SD下的album目录下的crash目录中。想要自己指定到其他目录的,可以在AlbumApplication中的configCollectCrashInfo函数) 36 | 37 | 闪退日志命名格式:发生闪退的时间(yyyyMMddHHmm 年月日时分秒).log 38 | 39 | ![闪退后生成日志](http://g.hiphotos.baidu.com/image/pic/item/d0c8a786c9177f3ed17a360377cf3bc79f3d5676.jpg) 40 | 41 | **2.闪退日志回传服务器处理** 42 | 43 | > 目前已经提供闪退日志回传到远程服务器的接口,有需要可以自行在AlbumApplication配置作如下实现!(发生闪退时,会回调onCrash方法,可以在此方法中讲闪退信息传回服务器) 44 | 45 | ![配置log回传服务器](http://h.hiphotos.baidu.com/image/pic/item/dbb44aed2e738bd494f0643fa68b87d6267ff9ef.jpg) 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 | ![Collections中的排序API](http://f.hiphotos.baidu.com/image/pic/item/f9dcd100baa1cd11f75a7e98be12c8fcc2ce2d9d.jpg) 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 | ![接入友盟的App](http://b.hiphotos.baidu.com/image/pic/item/b3119313b07eca807488435f962397dda04483fc.jpg) 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 | ![AndroidAlbum](http://c.hiphotos.baidu.com/image/pic/item/377adab44aed2e7339a6e4948001a18b87d6fa3f.jpg) 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 | ![MVC](http://d.hiphotos.baidu.com/image/pic/item/9e3df8dcd100baa1961910034010b912c8fc2e54.jpg) 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 | ![MVP](http://g.hiphotos.baidu.com/image/pic/item/d833c895d143ad4baf58147885025aafa40f0617.jpg) 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 |