├── .gitignore ├── Demo_Intro_zh.md ├── README.md ├── README_zh.md ├── app ├── .gitignore ├── album.jks ├── apk │ ├── app-arm64-v8a-release.apk │ └── app-armeabi-v7a-release.apk ├── build.gradle ├── libs │ ├── dlib-release.aar │ ├── prob_det-dev-release.aar │ └── processing-release.aar ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── demo │ │ ├── IMG_2701.JPG │ │ ├── IMG_2702.JPG │ │ ├── IMG_2703.JPG │ │ ├── IMG_2708.JPG │ │ ├── IMG_2709.JPG │ │ ├── IMG_2710.JPG │ │ ├── IMG_2711.JPG │ │ ├── IMG_2712.JPG │ │ ├── IMG_2738.JPG │ │ ├── IMG_2740.JPG │ │ ├── IMG_2741.JPG │ │ └── d_sample.jpg │ └── polarr.html │ ├── ic_launcher-web.png │ ├── java │ └── co │ │ └── polarr │ │ └── albumsdkdemo │ │ ├── GroupPhotoAdapter.java │ │ ├── GroupingActivity.java │ │ ├── MainActivity.java │ │ ├── SingleGroupAdapter.java │ │ ├── entities │ │ └── GroupingResult.java │ │ └── utils │ │ ├── BenchmarkUtil.java │ │ ├── ExportUtil.java │ │ ├── FileUtils.java │ │ ├── ImageRenderUtil.java │ │ ├── ImageUtil.java │ │ ├── MemoryCache.java │ │ └── ScaledImageView.java │ └── res │ ├── layout │ ├── activity_grouping.xml │ ├── activity_main.xml │ ├── group_photo_item.xml │ └── single_group_item.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values-zh │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── ids.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── filepaths.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── release └── polarr_album_v1.1.17.apk ├── rtdemo ├── .gitignore ├── README.md ├── README_zh.md ├── build.gradle ├── key.jks ├── libs │ └── processing-release.aar ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── co │ │ └── polarr │ │ └── demo │ │ └── rtfeature │ │ ├── CameraView.java │ │ └── MainActivity.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── settings.gradle └── tflibs ├── arm64-v8a └── libtensorflow_inference.so └── armeabi-v7a └── libtensorflow_inference.so /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | *idea 4 | /local.properties 5 | /.idea/workspace.xml 6 | /.idea/libraries 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | -------------------------------------------------------------------------------- /Demo_Intro_zh.md: -------------------------------------------------------------------------------- 1 | # Demo使用说明 2 | 3 | ## 图片评分 4 | ![图片评分](https://user-images.githubusercontent.com/5923363/32413834-50df787c-c1df-11e7-9e58-1a41a07a1b68.jpeg) 5 | 6 | ## 图片分组 7 | ![图片分组](https://user-images.githubusercontent.com/5923363/32413835-51159f56-c1df-11e7-80fc-4043f6326e19.jpeg) 8 | 9 | ## 图片分组结果 10 | ![图片分组结果](https://user-images.githubusercontent.com/5923363/32413836-5150d954-c1df-11e7-9166-290642295d5f.jpeg) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PolarrAlbumAndroidSDK 2 | 3 | *As of January 2025, the public repo of Polarr Album Android SDK is no longer maintained. If you are interested in commercial licensing of the SDK, please reach out to [info@polarr.co](mailto:info@polarr.co)*. 4 | 5 | Polarr Android SDK for Smart Album - Includes photo auto grouping, tagging, rating and etc. The SDK serves as an arsenal for Android developers to leverage deep learning and machine learning to organize and enhance a set of photos. Polarr currently has an iOS App called [Polarr Album+](mailto:hello@polarr.ai) which showcases all functions of the SDK plus some. Feature requests are welcomed. 6 | 7 | This SDK includes a starter project (co.polarr.albumsdkdemo) that calls the Android SDK. 8 | 9 | The minimum Android API Level is 15 (4.0.3). 10 | 11 | ## License 12 | The SDK included in this repository must not be used for any commercial purposes without the direct written consent of Polarr, Inc. The current version of the SDK expires on December 31, 2024. For pricing and more info regarding the full license SDK, please email [hello@polarr.ai](mailto:hello@polarr.ai). 13 | 14 | ## Functionalities 15 | ### Tagging a photo 16 | The SDK performs image classification and produce the top 3 most likely labels for the given photo. It also returns an overall rating value from 1.0 to 5.0 (where 1.0 is the worst and 5.0 is the best), which is based on the following metrics: 17 | - Colorfulness 18 | - Exposure (well-exposed photo or poorly exposed: either under-exposed or over-exposed) 19 | - Clarity (bluriness of the photo) 20 | - Expression (if faces are detected, are they smiling, are the eyes open) 21 | 22 | 23 | 24 | 25 | ### Grouping photos 26 | 27 | Similar photos are grouped together based on their subjects, features, colors, and other metrics. 28 | 29 | 30 | 31 | ## Add dependencies to Gradle 32 | ```groovy 33 | dependencies { 34 | // photo processing lib 35 | compile(name: 'processing-dev-release', ext: 'aar') 36 | // photo detection lib 37 | compile(name: 'prob_det-release', ext: 'aar') 38 | // Tensorflow lib 39 | compile 'org.tensorflow:tensorflow-android:+' 40 | // Face detection lib 41 | compile(name: 'dlib-release', ext: 'aar') 42 | } 43 | ``` 44 | 45 | ## Rating photo 46 | Rating a photo file. The score from 1.0 to 5.0. 47 | ### Rating by file path 48 | ```java 49 | String filePath; 50 | boolean isBurst = false; 51 | Map result = Processing.processingFile(context, filePath, isBurst); 52 | ``` 53 | ### Rating by bitmap 54 | Need a fit scaled bimap. The max width or height less then 300px. It will resize to 300px if exceeded. 55 | ```java 56 | Bitmap bitmap; 57 | long fileCreateTime; // millisecond. 58 | boolean isBurst = false; 59 | Map featureResult = Processing.processingFile(context, bitmap, fileCreateTime, isBurst); 60 | ``` 61 | ### Rating results 62 | ```java 63 | float metric_clarity = (float)result.get("metric_clarity"); 64 | float metric_exposure = (float)result.get("metric_exposure"); 65 | float metric_colorfulness = (float)result.get("metric_colorfulness"); 66 | float metric_emotion = (float)result.get("metric_emotion"); 67 | 68 | float rating_all = (float)result.get("rating_all"); 69 | ``` 70 | 71 | ## Tagging photo 72 | Recognize a photo, get top 3 possible objects from the photo 73 | ```java 74 | // scale photo to 224x224 75 | Bitmap bitmap = ImageUtil.getScaledBitmap(photo.getPath(), 224, 224); 76 | Map taggingResult = TaggingUtil.tagPhoto(context.getAssets(), bitmap); 77 | // marge the result with processing result 78 | result.putAll(taggingResult); 79 | ``` 80 | 81 | ## Grouping photos 82 | First, rating and tagging photo to a feature result. Then grouping the feature results. 83 | ### Feature a photo 84 | Join rating result and tagging result. 85 | ```java 86 | String filePath; 87 | boolean isBurst = false; 88 | Map featureResult = Processing.processingFile(context, filePath. isBurst); 89 | Bitmap bitmap = ImageUtil.getScaledBitmap(filePath, 224, 224); 90 | Map taggingResult = TaggingUtil.tagPhoto(getAssets(), bitmap); 91 | featureResult.putAll(taggingResult); 92 | ``` 93 | ### Grouping feature results 94 | #### init photo files 95 | ```java 96 | List realPhotos = new ArrayList<>(); 97 | ``` 98 | #### Get features of photos 99 | ```java 100 | List> features = new ArrayList<>(); 101 | boolean isBurst = false; 102 | for (File photo : realPhotos) { 103 | Map featureResult = Processing.processingFile(context, photo.getPath(). isBurst); 104 | Bitmap bitmap = ImageUtil.getScaledBitmap(photo.getPath(), 224, 224); 105 | Map taggingResult = TaggingUtil.tagPhoto(getAssets(), bitmap); 106 | 107 | featureResult.putAll(taggingResult); 108 | features.add(featureResult); 109 | } 110 | ``` 111 | #### Grouping photos 112 | ```java 113 | String identifier = "group1"; 114 | List> features = new ArrayList<>(); 115 | boolean isBurst = false; 116 | float sensitivity = 1f; //(0.1,1) 117 | GroupingResultItem result = Processing.processingGrouping(identifier, features, isBurst, sensitivity, new POGenerateHClusterCallbackFunction() { 118 | @Override 119 | public void progress(double progress) { 120 | // grouping progress 121 | } 122 | }); 123 | ``` 124 | #### Convert results 125 | ```java 126 | Map>> groups = result.groups; 127 | int opt = result.optimalGroupIndex; 128 | List> optGroups = groups.get(opt); 129 | List> groupdFiles = new ArrayList<>(); 130 | for (List subGroup : optGroups) { 131 | List sub = new ArrayList<>(); 132 | for (Integer index : subGroup) { 133 | ResultItem resultItem = new ResultItem(); 134 | resultItem.filePath = realPhotos.get(index).getPath(); 135 | resultItem.features = features.get(index); 136 | 137 | sub.add(resultItem); 138 | } 139 | groupdFiles.add(sub); 140 | } 141 | ``` 142 | #### Get the best one 143 | ```java 144 | ResultItem bestItem = Processing.getBest(groupdFiles); 145 | ``` 146 | #### Order photos 147 | ```java 148 | Processing.sortGroupsByScore(groupdFiles); 149 | ``` 150 | ### Order the photos with face detection 151 | ```java 152 | List> features = new ArrayList<>(); 153 | GroupingResultItem result = Processing.processingFaces(features); 154 | ``` 155 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # 泼辣相册AndroidSDK 2 | 泼辣相册 (Picky) - 通过先进的照片分析引擎对照片进行分类,打标签,打分。同时进行照片自动增强. 本SDK通过运用深度学习以及机器学习技术,提供给Android开发者照片分析以及图片增强功能。您可以在苹果应用商店下载到[《泼辣相册》](https://itunes.apple.com/cn/app/%E6%B3%BC%E8%BE%A3%E7%9B%B8%E5%86%8C/id1261219573?mt=8)。本SDK包含了泼辣相册的核心功能 3 | 4 | 本SDK目前尚为Alpha版,包含了泼辣相册的部分功能。未来几个月内,我们将持续更新更多的功能 5 | 6 | 本SDK包含了一个示例工程 (co.polarr.albumsdkdemo) 用于调试以及开发对接 7 | 8 | 最低版本限制 Android API Level 15 (4.0.3) 9 | 10 | ## 版权限制 11 | 包含本SDK在内的所有版本库中的内容,属于Polarr, Inc.版权所有。未经允许均不得用于商业目的。当前版本的示例SDK失效时间为2024年12月31日。如需要获取完整授权等更多相关信息,请联系我们[info@polarr.co](mailto:info@polarr.co) 12 | 13 | ## 功能模块 14 | ### 评分与打标签 15 | 本SDK提供图片评分功能。通过图片分析,计算出图片得分,分值从 1.0 至 5.0 (由差到好)。分析基于以下四个维度: 16 | - 图片色彩 17 | - 曝光度 (曝光充足或曝光不足,曝光过低或曝光过度) 18 | - 清晰度 (图像清晰模糊的程度) 19 | - 表现力 (如果照片中包含人脸, 评价是否微笑或睁开眼睛) 20 | 21 | ![图片评分](https://user-images.githubusercontent.com/5923363/32823120-64b4fc4a-c9a1-11e7-96c8-25514ac92979.png) 22 | 23 | ### 图片归类 24 | 根据图片的的特点,主题,色彩,评分进行相似图片归类,优质图片排序 25 | 26 | ![图片归类](https://user-images.githubusercontent.com/5923363/32823142-81f5a192-c9a1-11e7-9c72-89a113aaaa62.png) 27 | 28 | ## 增加 dependencies 到 Gradle 文件 29 | ```groovy 30 | dependencies { 31 | // 图片优选核心模块 32 | compile(name: 'processing-dev-release', ext: 'aar') 33 | // 人脸识别模块 34 | compile(name: 'dlib-release', ext: 'aar') 35 | // 图片分析辅助模块 36 | compile(name: 'prob_det-release', ext: 'aar') 37 | // TensorFlow运行库,用于图片分析 38 | compile 'org.tensorflow:tensorflow-android:+' 39 | } 40 | ``` 41 | 42 | ## 图片评分接口 43 | 给一个图片文件评分,分值从 1.0 至 5.0 44 | ### 传入图片文件路径 45 | ```java 46 | // 图片路径 47 | String filePath; 48 | // 是否快速模式(不分析人物表情) 49 | boolean isBurst = false; 50 | Map result = Processing.processingFile(context, filePath, isBurst); 51 | ``` 52 | ### 传入图片Bitmap和图片文件创建时间 53 | 图片需要等比例缩放,最佳大小为宽高不超过300px 54 | ```java 55 | Bitmap bitmap; 56 | long fileCreateTime;// 精确到 ms 毫秒 57 | // 是否快速模式(不分析人物表情) 58 | boolean isBurst = false; 59 | Map featureResult = Processing.processingFile(context, bitmap, fileCreateTime, isBurst); 60 | ``` 61 | ### 评分结果说明 62 | ```java 63 | // 清晰度得分 64 | float metric_clarity = (float)result.get("metric_clarity"); 65 | // 曝光得分 66 | float metric_exposure = (float)result.get("metric_exposure"); 67 | // 色彩得分 68 | float metric_colorfulness = (float)result.get("metric_colorfulness"); 69 | // 表情得分 70 | float metric_emotion = (float)result.get("metric_emotion"); 71 | 72 | // 总得分 73 | float rating_all = (float)result.get("rating_all"); 74 | ``` 75 | 76 | ## 获取图片标签,用于图片精准分类 77 | ```java 78 | // 将图片缩放到224x224大小 79 | Bitmap bitmap = ImageUtil.getScaledBitmap(photo.getPath(), 224, 224); 80 | // 获取图片标签 81 | Map taggingResult = TaggingUtil.tagPhoto(context.getAssets(), bitmap); 82 | // 将标签结果和评分结果合并 83 | result.putAll(taggingResult); 84 | ``` 85 | 86 | ## 图片归类 87 | 先对每张图片进行评分,打标签获取图片特性,之后对图片进行归类 88 | ### 获取图片特性 89 | 图片特性包括评分和打标签两部分组成 90 | ```java 91 | String filePath; 92 | // 是否快速模式(不分析人物表情) 93 | boolean isBurst = false; 94 | Map featureResult = Processing.processingFile(context, filePath. isBurst); 95 | Bitmap bitmap = ImageUtil.getScaledBitmap(filePath, 224, 224); 96 | Map taggingResult = TaggingUtil.tagPhoto(getAssets(), bitmap); 97 | featureResult.putAll(taggingResult); 98 | ``` 99 | ### 图片归类接口(图片回忆) 100 | #### 初始化图片文件 101 | ```java 102 | List realPhotos = new ArrayList<>(); 103 | ``` 104 | #### 获取图片属性信息 105 | ```java 106 | List> features = new ArrayList<>(); 107 | boolean isBurst = false; 108 | for (File photo : realPhotos) { 109 | Map featureResult = Processing.processingFile(context, photo.getPath(). isBurst); 110 | // 连拍归档不需要进行图片标签分析 111 | Bitmap bitmap = ImageUtil.getScaledBitmap(photo.getPath(), 224, 224); 112 | Map taggingResult = TaggingUtil.tagPhoto(getAssets(), bitmap); 113 | 114 | featureResult.putAll(taggingResult); 115 | features.add(featureResult); 116 | } 117 | ``` 118 | #### 图片分组,包含连拍归档 119 | ```java 120 | String identifier = "group1"; 121 | // 上一步获取的包含所有图片属性的数组 122 | List> features = new ArrayList<>(); 123 | // 是否快速模式,用于连拍归档 124 | boolean isBurst = false; 125 | // 分组敏感度,数值越低越不敏感。最佳值为1.0,取值范围为 0.01, 1。 126 | float sensitivity = 1f; //(0.01, 1.0) 127 | GroupingResultItem result = Processing.processingGrouping(identifier, features, isBurst, sensitivity, new POGenerateHClusterCallbackFunction() { 128 | @Override 129 | public void progress(double progress) { 130 | // grouping progress 131 | } 132 | }); 133 | ``` 134 | #### 将结果转化为对象 135 | ```java 136 | // 分组结果 137 | Map>> groups = result.groups; 138 | // 最佳结果的索引 139 | int opt = result.optimalGroupIndex; 140 | // 获得最佳的分组结果 141 | List> optGroups = groups.get(opt); 142 | List> groupdFiles = new ArrayList<>(); 143 | for (List subGroup : optGroups) { 144 | List sub = new ArrayList<>(); 145 | for (Integer index : subGroup) { 146 | ResultItem resultItem = new ResultItem(); 147 | resultItem.filePath = realPhotos.get(index).getPath(); 148 | resultItem.features = features.get(index); 149 | 150 | sub.add(resultItem); 151 | } 152 | groupdFiles.add(sub); 153 | } 154 | ``` 155 | #### 获取最佳照片 156 | ```java 157 | ResultItem bestItem = Processing.getBest(groupdFiles); 158 | ``` 159 | #### 按评价排列图片 160 | ```java 161 | Processing.sortGroupsByScore(groupdFiles); 162 | ``` 163 | ### 人物封面 164 | ```java 165 | // 包含所有图片属性的数组 166 | List> features = new ArrayList<>(); 167 | GroupingResultItem result = Processing.processingFaces(features); 168 | ``` 169 | 后续参考:[将结果转化为对象](#将结果转化为对象) 170 | 171 | ## 用第三方人脸识别库进行表情评分 172 | 用第三方表情识别库返回的数据进行表情评分 173 | 174 | 人脸数据,支持多张人脸,每张人脸数据的点个数必须为106个 175 | ```java 176 | List> facePoints = new ArrayList<>(); 177 | List faceRects = new ArrayList<>(); 178 | ``` 179 | 先进行[Processing.processingFile](#图片评分接口)处理,之后做表情评分分析。 如果为 `featureResult==null` 则表示只做表情评分,此时有返回值。 180 | ```java 181 | Map featureResult; 182 | ``` 183 | 如果进行人脸识别的图片尺寸与原始图片尺寸不一致,则需要调用这个接口 184 | ```java 185 | Processing.computeEmotion(featureResult, facePoints, faceRects, FACE_DET_WIDTH, FACE_DET_HEIGHT); 186 | ``` 187 | 如果进行人脸识别的图片尺寸与原始图片尺寸相同,则调用这个接口即可。此时必须要先进行 [Processing.processingFile](#图片评分接口) 188 | ```java 189 | Processing.computeEmotion(featureResult, facePoints, faceRects); 190 | ``` 191 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/album.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/album.jks -------------------------------------------------------------------------------- /app/apk/app-arm64-v8a-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/apk/app-arm64-v8a-release.apk -------------------------------------------------------------------------------- /app/apk/app-armeabi-v7a-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/apk/app-armeabi-v7a-release.apk -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 30 5 | buildToolsVersion "28.0.3" 6 | 7 | defaultConfig { 8 | applicationId "co.polarr.albumsdkdemo" 9 | minSdkVersion 23 10 | targetSdkVersion 30 11 | versionCode 23 12 | versionName "1.1.17" 13 | } 14 | signingConfigs { 15 | release { 16 | storeFile file('album.jks') 17 | storePassword 'polarr' 18 | keyAlias 'polarr' 19 | keyPassword 'polarr' 20 | } 21 | } 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 26 | signingConfig signingConfigs.release 27 | ndk { 28 | abiFilters "arm64-v8a" // 32bit: armeabi-v7a 64bit: arm64-v8a 29 | } 30 | applicationVariants.all { variant -> 31 | variant.outputs.all { 32 | outputFileName = "polarr_album_v${defaultConfig.versionName}.apk" 33 | } 34 | } 35 | } 36 | debug { 37 | signingConfig signingConfigs.release 38 | } 39 | } 40 | 41 | compileOptions { 42 | sourceCompatibility 1.8 43 | targetCompatibility 1.8 44 | } 45 | } 46 | 47 | repositories { 48 | flatDir { 49 | dirs 'libs' 50 | } 51 | } 52 | 53 | dependencies { 54 | implementation fileTree(dir: 'libs', include: ['*.jar']) 55 | implementation 'androidx.appcompat:appcompat:1.3.1' 56 | implementation 'com.google.android.material:material:1.4.0' 57 | implementation 'androidx.constraintlayout:constraintlayout:2.1.0' 58 | 59 | implementation 'com.obsez.android.lib.filechooser:filechooser:1.1.5' 60 | 61 | implementation(name: 'processing-release', ext: 'aar') 62 | implementation(name: 'dlib-release', ext: 'aar') 63 | implementation(name: 'prob_det-dev-release', ext: 'aar') 64 | } 65 | -------------------------------------------------------------------------------- /app/libs/dlib-release.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/libs/dlib-release.aar -------------------------------------------------------------------------------- /app/libs/prob_det-dev-release.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/libs/prob_det-dev-release.aar -------------------------------------------------------------------------------- /app/libs/processing-release.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/libs/processing-release.aar -------------------------------------------------------------------------------- /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 /Users/colinpro/Library/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 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/assets/demo/IMG_2701.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/src/main/assets/demo/IMG_2701.JPG -------------------------------------------------------------------------------- /app/src/main/assets/demo/IMG_2702.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/src/main/assets/demo/IMG_2702.JPG -------------------------------------------------------------------------------- /app/src/main/assets/demo/IMG_2703.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/src/main/assets/demo/IMG_2703.JPG -------------------------------------------------------------------------------- /app/src/main/assets/demo/IMG_2708.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/src/main/assets/demo/IMG_2708.JPG -------------------------------------------------------------------------------- /app/src/main/assets/demo/IMG_2709.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/src/main/assets/demo/IMG_2709.JPG -------------------------------------------------------------------------------- /app/src/main/assets/demo/IMG_2710.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/src/main/assets/demo/IMG_2710.JPG -------------------------------------------------------------------------------- /app/src/main/assets/demo/IMG_2711.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/src/main/assets/demo/IMG_2711.JPG -------------------------------------------------------------------------------- /app/src/main/assets/demo/IMG_2712.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/src/main/assets/demo/IMG_2712.JPG -------------------------------------------------------------------------------- /app/src/main/assets/demo/IMG_2738.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/src/main/assets/demo/IMG_2738.JPG -------------------------------------------------------------------------------- /app/src/main/assets/demo/IMG_2740.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/src/main/assets/demo/IMG_2740.JPG -------------------------------------------------------------------------------- /app/src/main/assets/demo/IMG_2741.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/src/main/assets/demo/IMG_2741.JPG -------------------------------------------------------------------------------- /app/src/main/assets/demo/d_sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/src/main/assets/demo/d_sample.jpg -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polarrco/PolarrAlbumAndroidSDK/a4e947daaafbc2253af14f37c329d00f18845f88/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/co/polarr/albumsdkdemo/GroupPhotoAdapter.java: -------------------------------------------------------------------------------- 1 | package co.polarr.albumsdkdemo; 2 | 3 | import android.content.Context; 4 | import androidx.recyclerview.widget.LinearLayoutManager; 5 | import androidx.recyclerview.widget.RecyclerView; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.TextView; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Locale; 14 | 15 | import co.polarr.albumsdkdemo.entities.GroupingResult; 16 | import co.polarr.processing.entities.ResultItem; 17 | 18 | /** 19 | * Created by Colin on 2017/3/9. 20 | * picky layout title adapter 21 | */ 22 | 23 | public class GroupPhotoAdapter extends RecyclerView.Adapter { 24 | private final GroupingResult mPhotoFiles; 25 | private ResultItem mBestItem; 26 | private Context mContext; 27 | private LayoutInflater mInflater; 28 | 29 | public GroupPhotoAdapter(Context context, GroupingResult photoFiles, ResultItem bestItem) { 30 | mContext = context; 31 | mInflater = LayoutInflater.from(context); 32 | mPhotoFiles = photoFiles; 33 | mBestItem = bestItem; 34 | } 35 | 36 | @Override 37 | public LayoutViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 38 | View view = mInflater.inflate(R.layout.group_photo_item, parent, false); 39 | 40 | return new LayoutViewHolder(view); 41 | } 42 | 43 | @Override 44 | public int getItemCount() { 45 | return mPhotoFiles.optFiles.size() + (mPhotoFiles.badFiles.isEmpty() ? 0 : 1) 46 | + (mPhotoFiles.droppedFiles.isEmpty() ? 0 : 1) + (mBestItem != null ? 1 : 0); 47 | } 48 | 49 | @Override 50 | public void onBindViewHolder(LayoutViewHolder holder, int position) { 51 | holder.updateView(position); 52 | } 53 | 54 | class LayoutViewHolder extends RecyclerView.ViewHolder { 55 | private RecyclerView photosCon; 56 | private TextView tvGroupName; 57 | 58 | public LayoutViewHolder(View itemView) { 59 | super(itemView); 60 | photosCon = (RecyclerView) itemView.findViewById(R.id.rv_photos); 61 | tvGroupName = (TextView) itemView.findViewById(R.id.tv_groupName); 62 | LinearLayoutManager linearLayoutManager = new LinearLayoutManager(mContext); 63 | linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); 64 | photosCon.setLayoutManager(linearLayoutManager); 65 | } 66 | 67 | void updateView(int index) { 68 | boolean isBadGroup = false; 69 | boolean isDrppedGroup = false; 70 | List files; 71 | 72 | if (index == 0 && mBestItem != null) { 73 | files = new ArrayList<>(); 74 | files.add(mBestItem); 75 | tvGroupName.setText("The best photo"); 76 | } else { 77 | if (mBestItem != null) { 78 | index--; 79 | } 80 | if (index < mPhotoFiles.optFiles.size()) { 81 | files = mPhotoFiles.optFiles.get(index); 82 | tvGroupName.setText(String.format(Locale.ENGLISH, "Group %d, total: %d photos:", index + 1, files.size())); 83 | } else if (index == mPhotoFiles.optFiles.size() && !mPhotoFiles.badFiles.isEmpty()) { 84 | files = mPhotoFiles.badFiles; 85 | tvGroupName.setText(String.format(Locale.ENGLISH, "Bad group %d photos:", files.size())); 86 | isBadGroup = true; 87 | } else { 88 | files = mPhotoFiles.droppedFiles; 89 | tvGroupName.setText(String.format(Locale.ENGLISH, "Dropped group %d photos:", files.size())); 90 | isDrppedGroup = true; 91 | } 92 | } 93 | 94 | SingleGroupAdapter singleGroupAdapter = new SingleGroupAdapter(mContext, files, isBadGroup, isDrppedGroup); 95 | photosCon.setAdapter(singleGroupAdapter); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/src/main/java/co/polarr/albumsdkdemo/GroupingActivity.java: -------------------------------------------------------------------------------- 1 | package co.polarr.albumsdkdemo; 2 | 3 | import android.content.Intent; 4 | import android.net.Uri; 5 | import android.os.Bundle; 6 | import androidx.core.content.FileProvider; 7 | import androidx.appcompat.app.AppCompatActivity; 8 | import androidx.recyclerview.widget.LinearLayoutManager; 9 | import androidx.recyclerview.widget.RecyclerView; 10 | import android.view.View; 11 | import android.widget.TextView; 12 | 13 | import java.io.File; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | import co.polarr.albumsdkdemo.entities.GroupingResult; 18 | import co.polarr.albumsdkdemo.utils.ExportUtil; 19 | import co.polarr.albumsdkdemo.utils.MemoryCache; 20 | import co.polarr.processing.Processing; 21 | import co.polarr.processing.entities.ResultItem; 22 | 23 | public class GroupingActivity extends AppCompatActivity { 24 | 25 | private RecyclerView groupCon; 26 | private GroupingResult photoFiles; 27 | private TextView tv_sortby; 28 | private boolean isBurst; 29 | private boolean isFaces; 30 | private ResultItem bestItem; 31 | 32 | @Override 33 | protected void onCreate(Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.activity_grouping); 36 | 37 | groupCon = (RecyclerView) findViewById(R.id.rv_photos); 38 | tv_sortby = (TextView) findViewById(R.id.tv_sortby); 39 | LinearLayoutManager linearLayoutManager = new LinearLayoutManager(GroupingActivity.this); 40 | linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); 41 | groupCon.setLayoutManager(linearLayoutManager); 42 | 43 | photoFiles = (GroupingResult) MemoryCache.get("group_files"); 44 | 45 | tv_sortby.setVisibility(View.VISIBLE); 46 | 47 | isBurst = getIntent().getBooleanExtra("BURST", false); 48 | isFaces = getIntent().getBooleanExtra("FACE", false); 49 | 50 | if (isBurst) { 51 | tv_sortby.setText("Burst mode."); 52 | 53 | } else if (isFaces) { 54 | tv_sortby.setText("Faces mode."); 55 | } else { 56 | tv_sortby.setText("Sorted by rating."); 57 | } 58 | if (isFaces) { 59 | bestItem = photoFiles.optFiles.get(0).get(0); 60 | } else if (isBurst) { 61 | bestItem = null; 62 | } else { 63 | bestItem = Processing.getBest(photoFiles.optFiles); 64 | Processing.sortGroupsByScore(photoFiles.optFiles); 65 | } 66 | groupCon.setAdapter(new GroupPhotoAdapter(GroupingActivity.this, photoFiles, bestItem)); 67 | } 68 | 69 | public void buttonHandle(View view) { 70 | switch (view.getId()) { 71 | case R.id.btn_sort_score: 72 | Processing.sortGroupsByScore(photoFiles.optFiles); 73 | groupCon.setAdapter(new GroupPhotoAdapter(GroupingActivity.this, photoFiles, bestItem)); 74 | tv_sortby.setVisibility(View.VISIBLE); 75 | if (isBurst) { 76 | tv_sortby.setText("BrustMode by: rating"); 77 | } else { 78 | tv_sortby.setText("Sorted by: rating"); 79 | } 80 | // showNotice(); 81 | break; 82 | case R.id.btn_sort_time: 83 | Processing.sortGroupsByTime(photoFiles.optFiles); 84 | groupCon.setAdapter(new GroupPhotoAdapter(GroupingActivity.this, photoFiles, bestItem)); 85 | tv_sortby.setVisibility(View.VISIBLE); 86 | if (isBurst) { 87 | tv_sortby.setText("BrustMode by: time"); 88 | } else { 89 | tv_sortby.setText("Sorted by: time"); 90 | } 91 | // showNotice(); 92 | break; 93 | case R.id.btn_export: 94 | List allItems = new ArrayList<>(); 95 | for (List optItem : photoFiles.optFiles) { 96 | allItems.addAll(optItem); 97 | } 98 | allItems.addAll(photoFiles.badFiles); 99 | allItems.addAll(photoFiles.droppedFiles); 100 | 101 | Intent shareIntent = new Intent(); 102 | shareIntent.setAction(Intent.ACTION_SEND); 103 | File path = new File(getFilesDir(), "files"); 104 | if(!path.exists()) { 105 | path.mkdir(); 106 | } 107 | File targetFile = new File(path, "A+SDK_Export.csv"); 108 | ExportUtil.ExportToCsv(allItems, targetFile); 109 | Uri uri = FileProvider.getUriForFile(getApplicationContext(), BuildConfig.APPLICATION_ID + ".GroupingActivity", targetFile); 110 | 111 | shareIntent.putExtra(Intent.EXTRA_STREAM, uri); 112 | shareIntent.setType("*/*"); 113 | startActivity(Intent.createChooser(shareIntent, "Send to...")); 114 | break; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/co/polarr/albumsdkdemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package co.polarr.albumsdkdemo; 2 | 3 | import android.Manifest; 4 | import android.app.AlertDialog; 5 | import android.content.DialogInterface; 6 | import android.content.Intent; 7 | import android.content.pm.PackageManager; 8 | import android.content.res.AssetManager; 9 | import android.graphics.Bitmap; 10 | import android.graphics.BitmapFactory; 11 | import android.graphics.PointF; 12 | import android.graphics.Rect; 13 | import android.net.Uri; 14 | import android.os.Bundle; 15 | import android.os.Environment; 16 | import androidx.annotation.NonNull; 17 | import androidx.core.app.ActivityCompat; 18 | import androidx.core.content.ContextCompat; 19 | import androidx.appcompat.app.AppCompatActivity; 20 | import android.text.Editable; 21 | import android.view.View; 22 | import android.widget.Button; 23 | import android.widget.EditText; 24 | import android.widget.ImageView; 25 | import android.widget.ProgressBar; 26 | import android.widget.ScrollView; 27 | import android.widget.TextView; 28 | 29 | import com.obsez.android.lib.filechooser.ChooserDialog; 30 | 31 | import java.io.File; 32 | import java.io.FileOutputStream; 33 | import java.io.IOException; 34 | import java.io.InputStream; 35 | import java.io.OutputStream; 36 | import java.util.ArrayList; 37 | import java.util.List; 38 | import java.util.Locale; 39 | import java.util.Map; 40 | 41 | import co.polarr.albumsdkdemo.entities.GroupingResult; 42 | import co.polarr.albumsdkdemo.utils.BenchmarkUtil; 43 | import co.polarr.albumsdkdemo.utils.FileUtils; 44 | import co.polarr.albumsdkdemo.utils.ImageUtil; 45 | import co.polarr.albumsdkdemo.utils.MemoryCache; 46 | import co.polarr.processing.POGenerateHClusterCallbackFunction; 47 | import co.polarr.processing.Processing; 48 | import co.polarr.processing.entities.GroupingResultItem; 49 | import co.polarr.processing.entities.ResultItem; 50 | import co.polarr.tagging.probdet.TaggingUtil; 51 | 52 | public class MainActivity extends AppCompatActivity { 53 | private static final int REQUEST_PHOTO = 1; 54 | private static final int REQUEST_PHOTOS_GROUP = 2; 55 | private static final int REQUEST_PHOTOS_FACE = 3; 56 | private static final int REQUEST_PHOTOS_BURST = 4; 57 | private static final int REQUEST_IMPORT_PHOTO = 5; 58 | private static final int LOG_MAX_CHARS = 10000; 59 | 60 | private static final int TEST_FACE_DET_WIDTH = 720; 61 | private static final int TEST_FACE_DET_HEIGHT = 960; 62 | private static final PointF[] TEST_SENSTIME_FACE_DET_POINTS = { 63 | new PointF(286.54385f, 361.40244f), new PointF(286.98926f, 370.95502f), new PointF(287.66284f, 380.5722f), new PointF(288.8452f, 390.11874f), new PointF(290.59595f, 399.60583f), new PointF(292.58813f, 409.04327f), new PointF(294.79718f, 418.42172f), new PointF(297.31805f, 427.75858f), new PointF(300.52127f, 436.86423f), new PointF(304.60583f, 445.5826f), new PointF(309.56546f, 453.7632f), new PointF(315.1762f, 461.54095f), new PointF(321.2893f, 468.77216f), new PointF(327.90924f, 475.54947f), new PointF(335.61063f, 480.95938f), new PointF(344.5748f, 484.00867f), new PointF(354.12518f, 485.0118f), new PointF(364.4589f, 484.03537f), new PointF(374.39117f, 480.9098f), new PointF(383.2429f, 475.47137f), new PointF(391.07263f, 468.60773f), new PointF(398.3251f, 461.15106f), new PointF(404.96265f, 453.04764f), new PointF(410.79282f, 444.40442f), new PointF(415.72287f, 435.14694f), new PointF(419.44952f, 425.44678f), new PointF(422.16202f, 415.46448f), new PointF(424.4317f, 405.33017f), new PointF(426.39014f, 395.1298f), new PointF(427.87985f, 384.8649f), new PointF(428.8516f, 374.49973f), new PointF(429.06696f, 364.12546f), new PointF(428.86285f, 353.8208f), new PointF(295.56638f, 354.43555f), new PointF(304.7123f, 346.115f), new PointF(316.51245f, 344.22446f), new PointF(328.42963f, 345.39853f), new PointF(339.70798f, 348.29095f), new PointF(366.42245f, 344.99475f), new PointF(378.44077f, 339.75504f), new PointF(391.2325f, 336.49878f), new PointF(404.2696f, 337.11374f), new PointF(415.27835f, 345.15005f), new PointF(352.52637f, 362.28363f), new PointF(352.42432f, 376.34515f), new PointF(352.28232f, 390.61414f), new PointF(352.0493f, 404.80865f), new PointF(337.2761f, 414.10254f), new PointF(344.88043f, 415.9044f), new PointF(352.78952f, 417.38397f), new PointF(361.22955f, 415.31042f), new PointF(369.71594f, 413.17322f), new PointF(308.23813f, 365.988f), new PointF(314.43814f, 360.0169f), new PointF(330.3685f, 360.5202f), new PointF(335.66235f, 367.1159f), new PointF(328.80783f, 369.59146f), new PointF(314.4073f, 369.74176f), new PointF(372.96698f, 363.60492f), new PointF(377.86545f, 355.58917f), new PointF(394.72934f, 352.37402f), new PointF(402.05685f, 357.57825f), new PointF(396.04874f, 362.6315f), new PointF(380.6767f, 364.8764f), new PointF(305.19937f, 353.27466f), new PointF(316.7307f, 351.9191f), new PointF(328.34464f, 352.38483f), new PointF(339.4216f, 353.99265f), new PointF(367.07736f, 350.9349f), new PointF(378.95068f, 347.15588f), new PointF(391.58194f, 344.47906f), new PointF(404.33224f, 344.4525f), new PointF(322.53348f, 358.07278f), new PointF(321.62204f, 370.6366f), new PointF(322.0506f, 365.20505f), new PointF(385.98596f, 351.6984f), new PointF(388.59607f, 364.87848f), new PointF(387.37802f, 359.03247f), new PointF(344.13632f, 364.67505f), new PointF(362.7452f, 362.9245f), new PointF(337.4274f, 397.70618f), new PointF(369.35043f, 396.54208f), new PointF(332.39542f, 407.87662f), new PointF(374.76157f, 406.22858f), new PointF(325.89536f, 438.50137f), new PointF(335.72464f, 435.0731f), new PointF(345.9736f, 433.19495f), new PointF(352.50787f, 434.28616f), new PointF(359.1929f, 432.96387f), new PointF(370.30127f, 434.28937f), new PointF(381.57416f, 437.40097f), new PointF(373.37506f, 445.39786f), new PointF(363.63495f, 451.29007f), new PointF(352.41733f, 453.4249f), new PointF(341.73892f, 452.04962f), new PointF(332.70715f, 446.4889f), new PointF(329.62732f, 439.2097f), new PointF(341.15405f, 439.66818f), new PointF(352.5827f, 440.88235f), new PointF(364.85266f, 439.2419f), new PointF(377.56366f, 438.37354f), new PointF(364.9507f, 440.5428f), new PointF(352.26904f, 442.23212f), new PointF(340.71368f, 441.1324f), new PointF(322.04477f, 365.2079f), new PointF(387.36346f, 359.03104f) 64 | }; 65 | private static final Rect TEST_SENSTIME_FACE_RECT = new Rect(280, 325, 427, 472); 66 | 67 | /** 68 | * debug log view 69 | */ 70 | private TextView outputView; 71 | private ScrollView outputCon; 72 | 73 | /** 74 | * show processing photo 75 | */ 76 | private ImageView thumbnailView; 77 | /** 78 | * show auto enhanced photo 79 | */ 80 | private ImageView autoEnhanceView; 81 | /** 82 | * open grouped photos view 83 | */ 84 | private Button btnGroupResult; 85 | private EditText grouping_max; 86 | private EditText grouping_result_min; 87 | private EditText grouping_result_max; 88 | private EditText sensitivity; 89 | // processing ui 90 | private View processing_con; 91 | private ProgressBar processing_pb; 92 | private TextView processing_tv; 93 | private boolean isProcessing; 94 | private final Object processingLock = new Object(); 95 | private Button btn_cancel; 96 | 97 | @Override 98 | protected void onCreate(Bundle savedInstanceState) { 99 | super.onCreate(savedInstanceState); 100 | BenchmarkUtil.init(this); 101 | setContentView(R.layout.activity_main); 102 | 103 | outputView = (TextView) findViewById(R.id.tf_output); 104 | outputCon = (ScrollView) findViewById(R.id.sv_output); 105 | thumbnailView = (ImageView) findViewById(R.id.iv_thumbnail); 106 | autoEnhanceView = (ImageView) findViewById(R.id.iv_autoenhance); 107 | btnGroupResult = (Button) findViewById(R.id.btn_group_result); 108 | btn_cancel = (Button) findViewById(R.id.btn_cancel); 109 | processing_con = findViewById(R.id.processing_con); 110 | processing_tv = (TextView) findViewById(R.id.processing_tv); 111 | processing_pb = (ProgressBar) findViewById(R.id.processing_pb); 112 | grouping_max = (EditText) findViewById(R.id.grouping_max); 113 | grouping_result_min = (EditText) findViewById(R.id.grouping_result_min); 114 | grouping_result_max = (EditText) findViewById(R.id.grouping_result_max); 115 | sensitivity = (EditText) findViewById(R.id.sensitivity); 116 | 117 | autoEnhanceView.setVisibility(View.GONE); 118 | btnGroupResult.setVisibility(View.GONE); 119 | processing_con.setVisibility(View.GONE); 120 | 121 | 122 | findViewById(R.id.btn_import).setOnClickListener(new View.OnClickListener() { 123 | @Override 124 | public void onClick(View v) { 125 | selectPhotos(false, false); 126 | } 127 | }); 128 | findViewById(R.id.btn_import_faces).setOnClickListener(new View.OnClickListener() { 129 | @Override 130 | public void onClick(View v) { 131 | selectPhotos(true, false); 132 | } 133 | }); 134 | findViewById(R.id.btn_import_burst).setOnClickListener(new View.OnClickListener() { 135 | @Override 136 | public void onClick(View v) { 137 | selectPhotos(false, true); 138 | } 139 | }); 140 | findViewById(R.id.btn_group_demo).setOnClickListener(new View.OnClickListener() { 141 | @Override 142 | public void onClick(View v) { 143 | grouping(new File(getFilesDir() + "/demo"), false, false); 144 | } 145 | }); 146 | findViewById(R.id.btn_tag).setOnClickListener(new View.OnClickListener() { 147 | @Override 148 | public void onClick(View v) { 149 | selectPhoto(); 150 | } 151 | }); 152 | findViewById(R.id.btn_tag_demo).setOnClickListener(new View.OnClickListener() { 153 | @Override 154 | public void onClick(View v) { 155 | ratingPhoto(new File(getFilesDir() + "/demo/d_sample.jpg")); 156 | } 157 | }); 158 | 159 | btn_cancel.setOnClickListener(new View.OnClickListener() { 160 | @Override 161 | public void onClick(View v) { 162 | if (isProcessing) { 163 | isProcessing = false; 164 | } 165 | } 166 | }); 167 | 168 | copyDemoAssets(); 169 | } 170 | 171 | private void copyDemoAssets() { 172 | AssetManager assetManager = getAssets(); 173 | 174 | InputStream in; 175 | OutputStream out; 176 | 177 | String[] files; 178 | try { 179 | File demoPath = new File(getFilesDir() + "/demo"); 180 | files = getAssets().list("demo"); 181 | demoPath.mkdirs(); 182 | 183 | for (File f : demoPath.listFiles()) { 184 | f.delete(); 185 | } 186 | for (String fileName : files) { 187 | File file = new File(getFilesDir(), "/demo/" + fileName); 188 | try { 189 | in = assetManager.open("demo/" + fileName); 190 | out = new FileOutputStream(file); 191 | 192 | copyFile(in, out); 193 | in.close(); 194 | out.flush(); 195 | out.close(); 196 | } catch (Exception e) { 197 | e.printStackTrace(); 198 | } 199 | } 200 | 201 | } catch (IOException e) { 202 | e.printStackTrace(); 203 | } 204 | } 205 | 206 | private void copyFile(InputStream in, OutputStream out) throws IOException { 207 | byte[] buffer = new byte[1024]; 208 | int read; 209 | while ((read = in.read(buffer)) != -1) { 210 | out.write(buffer, 0, read); 211 | } 212 | } 213 | 214 | /** 215 | * photo folder chooser 216 | */ 217 | private void selectPhotos(final boolean isFaces, final boolean isBurst) { 218 | if (!checkAndRequirePermission(isFaces ? REQUEST_PHOTOS_FACE : isBurst ? REQUEST_PHOTOS_BURST : REQUEST_PHOTOS_GROUP)) { 219 | return; 220 | } 221 | new ChooserDialog().with(this) 222 | .withFilter(true, false) 223 | .withStartFile(Environment.getExternalStorageDirectory().getPath() + "/DCIM") 224 | .withChosenListener(new ChooserDialog.Result() { 225 | @Override 226 | public void onChoosePath(String path, File pathFile) { 227 | grouping(pathFile, isFaces, isBurst); 228 | } 229 | }) 230 | .build() 231 | .show(); 232 | } 233 | 234 | /** 235 | * photo chooser 236 | */ 237 | private void selectPhoto() { 238 | if (!checkAndRequirePermission(REQUEST_PHOTO)) { 239 | return; 240 | } 241 | 242 | Intent intent = new Intent(); 243 | intent.setType("image/*"); 244 | intent.setAction(Intent.ACTION_GET_CONTENT); 245 | startActivityForResult(Intent.createChooser(intent, "Select Photo"), REQUEST_IMPORT_PHOTO); 246 | } 247 | 248 | @Override 249 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 250 | if (resultCode == RESULT_OK) { 251 | if (requestCode == REQUEST_IMPORT_PHOTO) { 252 | 253 | Uri imageUri = data.getData(); 254 | if (imageUri != null) { 255 | String filePath = FileUtils.getPath(this, imageUri); 256 | 257 | ratingPhoto(new File(filePath)); 258 | } 259 | } 260 | } 261 | 262 | } 263 | 264 | private boolean checkAndRequirePermission(int permissionRequestId) { 265 | if (ContextCompat.checkSelfPermission(this, 266 | Manifest.permission.READ_EXTERNAL_STORAGE) 267 | != PackageManager.PERMISSION_GRANTED) { 268 | 269 | ActivityCompat.requestPermissions(this, 270 | new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 271 | permissionRequestId); 272 | 273 | return false; 274 | } 275 | 276 | return true; 277 | } 278 | 279 | @Override 280 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 281 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 282 | if (requestCode == REQUEST_PHOTO && grantResults.length > 0) { 283 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 284 | selectPhoto(); 285 | } 286 | } else if ((requestCode == REQUEST_PHOTOS_FACE || requestCode == REQUEST_PHOTOS_GROUP || requestCode == REQUEST_PHOTOS_BURST) && grantResults.length > 0) { 287 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 288 | selectPhotos(requestCode == REQUEST_PHOTOS_FACE, requestCode == REQUEST_PHOTOS_BURST); 289 | } 290 | } 291 | } 292 | 293 | /** 294 | * clear all logs 295 | */ 296 | private void clearOutput() { 297 | outputView.setText(""); 298 | } 299 | 300 | /** 301 | * show output logs 302 | */ 303 | private void output(final String text) { 304 | runOnUiThread(new Runnable() { 305 | @Override 306 | public void run() { 307 | String currentText = outputView.getText().toString(); 308 | if (currentText.length() - LOG_MAX_CHARS > 1000) { 309 | currentText = currentText.substring(currentText.length() - LOG_MAX_CHARS, currentText.length()); 310 | } 311 | currentText += "\n" + text; 312 | outputView.setText(currentText); 313 | 314 | outputView.post(new Runnable() { 315 | @Override 316 | public void run() { 317 | outputCon.fullScroll(View.FOCUS_DOWN); 318 | } 319 | }); 320 | } 321 | }); 322 | } 323 | 324 | /** 325 | * show current photo 326 | */ 327 | private void updateThumbnail(File file) { 328 | final Bitmap bitmap = ImageUtil.decodeThumbBitmapForFile(file.getPath(), 500, 500); 329 | runOnUiThread(new Runnable() { 330 | @Override 331 | public void run() { 332 | thumbnailView.setImageBitmap(bitmap); 333 | } 334 | }); 335 | } 336 | 337 | 338 | private void updateThumbnail(final Bitmap bitmap) { 339 | runOnUiThread(new Runnable() { 340 | @Override 341 | public void run() { 342 | thumbnailView.setImageBitmap(bitmap); 343 | } 344 | }); 345 | } 346 | 347 | /** 348 | * processing photo, get photo rating and tagging 349 | */ 350 | private void ratingPhoto(final File file) { 351 | clearOutput(); 352 | btnGroupResult.setVisibility(View.GONE); 353 | autoEnhanceView.setVisibility(View.GONE); 354 | 355 | new Thread(new Runnable() { 356 | @Override 357 | public void run() { 358 | output("start processing..."); 359 | updateThumbnail(file); 360 | BenchmarkUtil.TimeStart("processingFile"); 361 | BenchmarkUtil.MemStart("processingFile"); 362 | final Map result = Processing.processingFile(MainActivity.this, file.getPath(), false); 363 | 364 | BenchmarkUtil.MemEnd("processingFile"); 365 | 366 | if (result.containsKey("metric_emotion")) { 367 | float emotion = (float) result.get("metric_emotion"); 368 | if (emotion <= 0) { 369 | final Bitmap faceBm = ImageUtil.decodeThumbBitmapForFile(file.getPath(), 800, 800); 370 | Map faceResult = Processing.faceDetection(MainActivity.this, faceBm); 371 | if (faceResult.containsKey("metric_emotion")) { 372 | emotion = (float) faceResult.get("metric_emotion"); 373 | if (emotion > 0) { 374 | float total = (float) result.get("rating_all"); 375 | total += emotion / 2; 376 | result.put("rating_all", total); 377 | } 378 | } 379 | result.putAll(faceResult); 380 | // faceBm.recycle(); 381 | } 382 | } 383 | result.remove("metric_faces"); 384 | 385 | // // TODO add senstime interface here 386 | // List> facePoints = new ArrayList<>(); 387 | // facePoints.add(Arrays.asList(TEST_SENSTIME_FACE_DET_POINTS)); 388 | // List faceRects = new ArrayList<>(); 389 | // faceRects.add(TEST_SENSTIME_FACE_RECT); 390 | // Processing.computeEmotion(result, facePoints, faceRects, TEST_FACE_DET_WIDTH, TEST_FACE_DET_HEIGHT); 391 | 392 | for (String label : result.keySet()) { 393 | if (label.startsWith("metric_")) { 394 | String showLable = label.substring("metric_".length()); 395 | output("\t" + showLable + ": " + result.get(label)); 396 | } 397 | } 398 | 399 | runOnUiThread(new Runnable() { 400 | @Override 401 | public void run() { 402 | thumbnailView.setOnClickListener(new View.OnClickListener() { 403 | @Override 404 | public void onClick(View v) { 405 | showResult(result, file.getName()); 406 | } 407 | }); 408 | 409 | showResult(result, file.getName()); 410 | } 411 | }); 412 | 413 | output("end processing."); 414 | } 415 | }).start(); 416 | } 417 | 418 | private void showResult(Map result, String fileName) { 419 | AlertDialog alertDialog = new AlertDialog.Builder(MainActivity.this).create(); 420 | alertDialog.setTitle("Rating Result"); 421 | alertDialog.setMessage(MainActivity.getRatingDisplayResult(result, false, fileName)); 422 | alertDialog.show(); 423 | } 424 | 425 | public static String getRatingDisplayResult(Map result, boolean isBad, String fileName) { 426 | StringBuilder sb = new StringBuilder(); 427 | 428 | if (isBad) { 429 | sb.append("Bad Reason:"); 430 | sb.append(result.get("grouping_bad_reason")); 431 | sb.append("\n"); 432 | } 433 | 434 | sb.append("\n"); 435 | 436 | if (result.containsKey("metric_clarity")) { 437 | sb.append("clarity:"); 438 | sb.append(result.get("metric_clarity")); 439 | sb.append("\n"); 440 | } 441 | if (result.containsKey("metric_exposure")) { 442 | sb.append("exposure:"); 443 | sb.append(result.get("metric_exposure")); 444 | sb.append("\n"); 445 | } 446 | if (result.containsKey("metric_colorfulness")) { 447 | sb.append("colorfulness:"); 448 | sb.append(result.get("metric_colorfulness")); 449 | sb.append("\n"); 450 | } 451 | if (result.containsKey("metric_emotion")) { 452 | float emotion = (float) result.get("metric_emotion"); 453 | if (emotion > 0) { 454 | sb.append("emotion:"); 455 | sb.append(emotion); 456 | sb.append("\n"); 457 | } 458 | } 459 | 460 | if (result.containsKey("rating_all")) { 461 | sb.append("\n"); 462 | sb.append("Overall:"); 463 | sb.append(result.get("rating_all")); 464 | } 465 | 466 | if (fileName != null) { 467 | sb.append("\n"); 468 | sb.append("File:"); 469 | sb.append(new File(fileName).getName()); 470 | } 471 | 472 | return sb.toString(); 473 | } 474 | 475 | /** 476 | * processing and groupping photos 477 | * 478 | * @param folderPath folder of photos 479 | */ 480 | private void grouping(final File folderPath, final boolean isFaces, final boolean isBurst) { 481 | if (isProcessing) { 482 | return; 483 | } 484 | 485 | clearOutput(); 486 | btnGroupResult.setVisibility(View.GONE); 487 | autoEnhanceView.setVisibility(View.GONE); 488 | 489 | processing_con.setVisibility(View.VISIBLE); 490 | btn_cancel.setVisibility(View.VISIBLE); 491 | isProcessing = true; 492 | new Thread(new Runnable() { 493 | @Override 494 | public void run() { 495 | synchronized (processingLock) { 496 | output("Prepare photos..."); 497 | runOnUiThread(new Runnable() { 498 | @Override 499 | public void run() { 500 | processing_tv.setText("Prepare photos..."); 501 | } 502 | }); 503 | File[] photoFiles = folderPath.listFiles(); 504 | List realPhotos = new ArrayList<>(); 505 | List nonPhotos = new ArrayList<>(); 506 | Editable maxStr = grouping_max.getText(); 507 | 508 | int maxProcessing = 30; 509 | try { 510 | maxProcessing = Integer.parseInt(maxStr.toString()); 511 | } catch (Exception e) { 512 | e.printStackTrace(); 513 | 514 | maxProcessing = 30; 515 | final int finalMaxProcessing = maxProcessing; 516 | runOnUiThread(new Runnable() { 517 | @Override 518 | public void run() { 519 | grouping_max.setText(String.valueOf(finalMaxProcessing)); 520 | } 521 | }); 522 | } 523 | 524 | 525 | Editable editStr; 526 | 527 | editStr = grouping_result_min.getText(); 528 | 529 | int minResult = 5; 530 | try { 531 | minResult = Integer.parseInt(editStr.toString()); 532 | } catch (Exception e) { 533 | e.printStackTrace(); 534 | 535 | minResult = 5; 536 | final int finalMinResult = minResult; 537 | runOnUiThread(new Runnable() { 538 | @Override 539 | public void run() { 540 | grouping_result_min.setText(String.valueOf(finalMinResult)); 541 | } 542 | }); 543 | } 544 | 545 | editStr = grouping_result_max.getText(); 546 | 547 | int maxResult = 50; 548 | try { 549 | maxResult = Integer.parseInt(editStr.toString()); 550 | } catch (Exception e) { 551 | e.printStackTrace(); 552 | 553 | maxResult = 50; 554 | } 555 | 556 | maxResult = Math.max(maxResult, minResult); 557 | final int finalMaxResult = maxResult; 558 | runOnUiThread(new Runnable() { 559 | @Override 560 | public void run() { 561 | grouping_result_max.setText(String.valueOf(finalMaxResult)); 562 | } 563 | }); 564 | 565 | int directFiles = 0; 566 | for (File photo : photoFiles) { 567 | if (photo.isDirectory()) { 568 | directFiles++; 569 | continue; 570 | } 571 | BitmapFactory.Options size = ImageUtil.decodeImageSize(photo.getPath()); 572 | 573 | if (size.outWidth > 0 && size.outHeight > 0) { 574 | realPhotos.add(photo); 575 | } else { 576 | nonPhotos.add(photo); 577 | } 578 | if (--maxProcessing == 0) { 579 | break; 580 | } 581 | } 582 | 583 | if (realPhotos.size() < (photoFiles.length - directFiles)) { 584 | for (File nonPhoto : nonPhotos) { 585 | output("cannot decode:" + nonPhoto.getName()); 586 | } 587 | final int totalCount = photoFiles.length - directFiles; 588 | final int photoCount = realPhotos.size(); 589 | try { 590 | runOnUiThread(new Runnable() { 591 | @Override 592 | public void run() { 593 | AlertDialog alertDialog = new AlertDialog.Builder(MainActivity.this).create(); 594 | alertDialog.setTitle("Notice"); 595 | alertDialog.setMessage(String.format(Locale.ENGLISH, "There are %d files in the folder, only %d/%d can be decoded!\nCheck the not decoded files in the bottom. Need continue?", totalCount, photoCount, totalCount)); 596 | alertDialog.setButton(DialogInterface.BUTTON_POSITIVE, "Continue", new DialogInterface.OnClickListener() { 597 | @Override 598 | public void onClick(DialogInterface dialog, int which) { 599 | synchronized (processingLock) { 600 | processingLock.notify(); 601 | } 602 | } 603 | }); 604 | alertDialog.setButton(DialogInterface.BUTTON_NEGATIVE, "Cancel", new DialogInterface.OnClickListener() { 605 | @Override 606 | public void onClick(DialogInterface dialog, int which) { 607 | dialog.cancel(); 608 | } 609 | }); 610 | alertDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { 611 | @Override 612 | public void onCancel(DialogInterface dialog) { 613 | isProcessing = false; 614 | synchronized (processingLock) { 615 | processingLock.notify(); 616 | } 617 | } 618 | }); 619 | alertDialog.show(); 620 | } 621 | }); 622 | processingLock.wait(); 623 | } catch (InterruptedException e) { 624 | e.printStackTrace(); 625 | } 626 | } 627 | 628 | if (realPhotos.isEmpty()) { 629 | runOnUiThread(new Runnable() { 630 | @Override 631 | public void run() { 632 | AlertDialog alertDialog = new AlertDialog.Builder(MainActivity.this).create(); 633 | alertDialog.setTitle("Notice"); 634 | alertDialog.setMessage("There is no photo can be proceed!"); 635 | alertDialog.show(); 636 | } 637 | }); 638 | isProcessing = false; 639 | checkCancel(); 640 | return; 641 | } 642 | 643 | if (!checkCancel()) { 644 | return; 645 | } 646 | 647 | List> features = new ArrayList<>(); 648 | 649 | final int total = realPhotos.size(); 650 | output("Processing photo features..."); 651 | int currentIndex = 0; 652 | for (File photo : realPhotos) { 653 | Bitmap bitmap = ImageUtil.getScaledFitBitmap(photo.getPath(), 300, 300); 654 | updateThumbnail(bitmap); 655 | output("\tProcessing " + photo.getPath() + "..."); 656 | 657 | long fileCreateTime = ImageUtil.getPhotoCreationTime(photo); 658 | 659 | BenchmarkUtil.TimeStart("processingFile"); 660 | final Map featureResult = Processing.processingFile(MainActivity.this, bitmap, fileCreateTime, !isFaces && isBurst); 661 | BenchmarkUtil.TimeEnd("processingFile"); 662 | 663 | // TODO add senstime interface here 664 | // List> facePoints = new ArrayList<>(); 665 | // List faceRects = new ArrayList<>(); 666 | // Processing.computeEmotion(featureResult, facePoints, faceRects); 667 | 668 | if (!isBurst) { 669 | if (featureResult.containsKey("metric_emotion")) { 670 | float emotion = (float) featureResult.get("metric_emotion"); 671 | if (emotion <= 0) { 672 | final Bitmap faceBm = ImageUtil.decodeThumbBitmapForFile(photo.getPath(), 800, 800); 673 | BenchmarkUtil.TimeStart("faceDetection"); 674 | Map faceResult = Processing.faceDetection(MainActivity.this, faceBm); 675 | BenchmarkUtil.TimeEnd("faceDetection"); 676 | if (faceResult.containsKey("metric_emotion")) { 677 | emotion = (float) faceResult.get("metric_emotion"); 678 | if (emotion > 0) { 679 | float rating_all = (float) featureResult.get("rating_all"); 680 | rating_all += emotion / 2; 681 | featureResult.put("rating_all", rating_all); 682 | } 683 | } 684 | featureResult.putAll(faceResult); 685 | // faceBm.recycle(); 686 | } 687 | } 688 | } 689 | 690 | if (!isFaces && !isBurst) { 691 | bitmap = ImageUtil.getScaledBitmap(photo.getPath(), 224, 224); 692 | BenchmarkUtil.TimeStart("tagPhoto"); 693 | Map taggingResult = TaggingUtil.tagPhoto(getAssets(), bitmap); 694 | BenchmarkUtil.TimeEnd("tagPhoto"); 695 | // bitmap.recycle(); 696 | featureResult.putAll(taggingResult); 697 | } 698 | 699 | features.add(featureResult); 700 | currentIndex++; 701 | final int finalIndex = currentIndex; 702 | runOnUiThread(new Runnable() { 703 | @Override 704 | public void run() { 705 | int progress = finalIndex * 100 / total; 706 | processing_tv.setText("Processing photo features, " + finalIndex + "/" + total + " photos..."); 707 | 708 | processing_pb.setProgress(progress); 709 | } 710 | }); 711 | 712 | if (!checkCancel()) { 713 | return; 714 | } 715 | } 716 | 717 | runOnUiThread(new Runnable() { 718 | @Override 719 | public void run() { 720 | processing_tv.setText("Grouping photos..."); 721 | } 722 | }); 723 | 724 | output("Grouping photos..."); 725 | 726 | String identifier = "identifier"; 727 | GroupingResultItem result = null; 728 | try { 729 | if (isFaces) { 730 | BenchmarkUtil.TimeStart("processingFaces"); 731 | result = Processing.processingFaces(features); 732 | BenchmarkUtil.TimeEnd("processingFaces"); 733 | } else { 734 | 735 | BenchmarkUtil.TimeStart("processingGrouping"); 736 | BenchmarkUtil.MemStart("processingGrouping"); 737 | result = Processing.processingGrouping(identifier, features, isBurst, Float.parseFloat(sensitivity.getText().toString()), new POGenerateHClusterCallbackFunction() { 738 | @Override 739 | public void progress(final double progress) { 740 | output(String.format(Locale.ENGLISH, "Processing %.2f%%", progress * 100)); 741 | 742 | runOnUiThread(new Runnable() { 743 | @Override 744 | public void run() { 745 | processing_pb.setProgress((int) (progress * 100)); 746 | } 747 | }); 748 | 749 | if (!checkCancel()) { 750 | throw new IllegalStateException("cancel"); 751 | } 752 | } 753 | }); 754 | BenchmarkUtil.TimeEnd("processingGrouping"); 755 | BenchmarkUtil.MemEnd("processingGrouping"); 756 | } 757 | } catch (IllegalStateException e) { 758 | if (e.getMessage().equals("cancel")) { 759 | return; 760 | } else { 761 | throw e; 762 | } 763 | } 764 | 765 | if (!checkCancel()) { 766 | return; 767 | } 768 | output("Grouping result:" + result.toString()); 769 | 770 | Map>> groups = result.groups; 771 | List> finalResult; 772 | int opt = result.optimalGroupIndex; 773 | if (!isFaces && !isBurst) { 774 | if (groups.size() < minResult) { 775 | finalResult = groups.get(groups.size()); 776 | } else { 777 | if (opt > maxResult) { 778 | finalResult = groups.get(opt); 779 | } else if (opt < minResult) { 780 | finalResult = groups.get(minResult); 781 | } else { 782 | finalResult = groups.get(opt); 783 | } 784 | } 785 | } else { 786 | finalResult = groups.get(opt); 787 | } 788 | 789 | 790 | GroupingResult groupingResult = new GroupingResult(); 791 | List grouped = new ArrayList<>(); 792 | List> groupdFiles = new ArrayList<>(); 793 | for (List subGroup : finalResult) { 794 | List sub = new ArrayList<>(); 795 | for (Integer index : subGroup) { 796 | ResultItem resultItem = new ResultItem(); 797 | resultItem.filePath = realPhotos.get(index).getPath(); 798 | resultItem.features = features.get(index); 799 | grouped.add(index); 800 | 801 | sub.add(resultItem); 802 | } 803 | groupdFiles.add(sub); 804 | } 805 | 806 | if (!isFaces && !isBurst) { 807 | List droppedGroup = new ArrayList<>(); 808 | if (opt > maxResult) { 809 | Processing.sortGroupsByScore(groupdFiles); 810 | while (groupdFiles.size() > maxResult) { 811 | List lastGroup = groupdFiles.get(groupdFiles.size() - 1); 812 | droppedGroup.addAll(lastGroup); 813 | groupdFiles.remove(lastGroup); 814 | } 815 | } 816 | groupingResult.droppedFiles.addAll(droppedGroup); 817 | } 818 | 819 | groupingResult.optFiles.addAll(groupdFiles); 820 | 821 | for (int i = 0; i < realPhotos.size(); i++) { 822 | if (!grouped.contains(i)) { 823 | ResultItem resultItem = new ResultItem(); 824 | resultItem.filePath = realPhotos.get(i).getPath(); 825 | resultItem.features = features.get(i); 826 | 827 | groupingResult.badFiles.add(resultItem); 828 | } 829 | } 830 | 831 | 832 | MemoryCache.put("group_files", groupingResult); 833 | 834 | output("Optimal group:" + groupingResult.optFiles.toString()); 835 | 836 | runOnUiThread(new Runnable() { 837 | @Override 838 | public void run() { 839 | btnGroupResult.setVisibility(View.VISIBLE); 840 | btnGroupResult.setOnClickListener(new View.OnClickListener() { 841 | @Override 842 | public void onClick(View v) { 843 | // testGrouping(); 844 | Intent intent = new Intent(MainActivity.this, GroupingActivity.class); 845 | intent.putExtra("BURST", isBurst); 846 | intent.putExtra("FACE", isFaces); 847 | 848 | startActivity(intent); 849 | } 850 | }); 851 | } 852 | }); 853 | 854 | runOnUiThread(new Runnable() { 855 | @Override 856 | public void run() { 857 | processing_tv.setText("Processing finished!"); 858 | btn_cancel.setVisibility(View.GONE); 859 | } 860 | }); 861 | 862 | output("end processing.\nClick 'Grouping result' button to see the result."); 863 | isProcessing = false; 864 | processing_con.postDelayed(new Runnable() { 865 | @Override 866 | public void run() { 867 | processing_con.setVisibility(View.GONE); 868 | } 869 | }, 3000); 870 | } 871 | } 872 | }).start(); 873 | } 874 | 875 | private boolean checkCancel() { 876 | if (!isProcessing) { 877 | output("Processing canneled!"); 878 | 879 | runOnUiThread(new Runnable() { 880 | @Override 881 | public void run() { 882 | processing_tv.setText("Processing canneled!"); 883 | btn_cancel.setVisibility(View.GONE); 884 | } 885 | }); 886 | 887 | processing_con.postDelayed(new Runnable() { 888 | @Override 889 | public void run() { 890 | processing_con.setVisibility(View.GONE); 891 | } 892 | }, 3000); 893 | } 894 | 895 | return isProcessing; 896 | } 897 | } 898 | -------------------------------------------------------------------------------- /app/src/main/java/co/polarr/albumsdkdemo/SingleGroupAdapter.java: -------------------------------------------------------------------------------- 1 | package co.polarr.albumsdkdemo; 2 | 3 | import android.app.ActionBar; 4 | import android.app.AlertDialog; 5 | import android.app.Dialog; 6 | import android.content.Context; 7 | import android.content.DialogInterface; 8 | import androidx.recyclerview.widget.RecyclerView; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.widget.ImageView; 13 | import android.widget.TextView; 14 | 15 | import java.util.List; 16 | import java.util.Locale; 17 | import java.util.Map; 18 | 19 | import co.polarr.albumsdkdemo.utils.ImageRenderUtil; 20 | import co.polarr.albumsdkdemo.utils.ImageUtil; 21 | import co.polarr.albumsdkdemo.utils.ScaledImageView; 22 | import co.polarr.processing.entities.ResultItem; 23 | 24 | /** 25 | * Created by Colin on 2017/3/9. 26 | * picky layout title adapter 27 | */ 28 | 29 | public class SingleGroupAdapter extends RecyclerView.Adapter { 30 | private static final int MAX_PREVIEW_SIZE = 2048; 31 | private final List mPhotos; 32 | private boolean mIsBad; 33 | private boolean mIsDropped; 34 | private Context mContext; 35 | private LayoutInflater mInflater; 36 | 37 | public SingleGroupAdapter(Context context, List photoFiles, boolean isBadGroup, boolean isDrppedGroup) { 38 | mContext = context; 39 | mInflater = LayoutInflater.from(context); 40 | mPhotos = photoFiles; 41 | mIsBad = isBadGroup; 42 | mIsDropped = isDrppedGroup; 43 | } 44 | 45 | @Override 46 | public LayoutViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 47 | View view = mInflater.inflate(R.layout.single_group_item, parent, false); 48 | 49 | return new LayoutViewHolder(view); 50 | } 51 | 52 | @Override 53 | public int getItemCount() { 54 | return mPhotos.size(); 55 | } 56 | 57 | @Override 58 | public void onBindViewHolder(LayoutViewHolder holder, int position) { 59 | holder.updateView(position); 60 | } 61 | 62 | class LayoutViewHolder extends RecyclerView.ViewHolder { 63 | private TextView score_tv; 64 | private View star_iv; 65 | private ImageView photoCon; 66 | 67 | public LayoutViewHolder(View itemView) { 68 | super(itemView); 69 | photoCon = (ImageView) itemView.findViewById(R.id.iv_photo); 70 | score_tv = (TextView) itemView.findViewById(R.id.score_tv); 71 | star_iv = itemView.findViewById(R.id.star_iv); 72 | } 73 | 74 | void updateView(int index) { 75 | if (index == 0 && !mIsBad && !mIsDropped) { 76 | star_iv.setVisibility(View.VISIBLE); 77 | } else { 78 | star_iv.setVisibility(View.INVISIBLE); 79 | } 80 | final String photoPath = mPhotos.get(index).filePath; 81 | final Map features = mPhotos.get(index).features; 82 | if (features.containsKey("aesthetics_score")) { 83 | score_tv.setText(String.format(Locale.ENGLISH, "Score: %.2f", (float) features.get("aesthetics_score") * 100)); 84 | } else { 85 | String scoreStr = String.format(Locale.ENGLISH, "%.1f|%.1f|%.1f", 86 | (float) features.get("metric_clarity"), 87 | (float) features.get("metric_exposure"), 88 | (float) features.get("metric_colorfulness")); 89 | score_tv.setText(scoreStr); 90 | } 91 | score_tv.setVisibility(View.GONE); 92 | photoCon.setOnClickListener(new View.OnClickListener() { 93 | @Override 94 | public void onClick(View v) { 95 | final AlertDialog alertDialog = new AlertDialog.Builder(mContext) 96 | .setPositiveButton("Show Original", null) 97 | .create(); 98 | alertDialog.setTitle("Rating Result"); 99 | alertDialog.setMessage(MainActivity.getRatingDisplayResult(features, mIsBad, photoPath)); 100 | alertDialog.setOnShowListener(new DialogInterface.OnShowListener() { 101 | @Override 102 | public void onShow(DialogInterface dialog) { 103 | alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { 104 | @Override 105 | public void onClick(View v) { 106 | showImage(photoPath); 107 | } 108 | }); 109 | } 110 | }); 111 | alertDialog.show(); 112 | } 113 | }); 114 | ImageRenderUtil.load().setPath(photoPath).setNeedRecyle(false).setSize(200, 200).into(photoCon); 115 | } 116 | } 117 | 118 | 119 | public void showImage(String photoPath) { 120 | // final AlertDialog dialog = new AlertDialog.Builder(mContext).create(); 121 | // ImageView imgView = getView(); 122 | // dialog.setView(imgView); 123 | // dialog.show(); 124 | final Dialog dialog = new Dialog(mContext, android.R.style.Theme_Black_NoTitleBar_Fullscreen); 125 | ImageView imgView = getView(photoPath); 126 | dialog.setContentView(imgView); 127 | dialog.show(); 128 | 129 | // imgView.setOnClickListener(new View.OnClickListener() { 130 | // @Override 131 | // public void onClick(View v) { 132 | // dialog.dismiss(); 133 | // } 134 | // }); 135 | } 136 | 137 | 138 | private ScaledImageView getView(String photoPath) { 139 | ScaledImageView imgView = new ScaledImageView(mContext); 140 | imgView.setLayoutParams(new ActionBar.LayoutParams(ActionBar.LayoutParams.MATCH_PARENT, ActionBar.LayoutParams.MATCH_PARENT)); 141 | imgView.setImageBitmap(ImageUtil.decodeThumbBitmapForFile(photoPath, MAX_PREVIEW_SIZE, MAX_PREVIEW_SIZE)); 142 | 143 | return imgView; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /app/src/main/java/co/polarr/albumsdkdemo/entities/GroupingResult.java: -------------------------------------------------------------------------------- 1 | package co.polarr.albumsdkdemo.entities; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import co.polarr.processing.entities.ResultItem; 7 | 8 | /** 9 | * Created by Colin on 2017/10/21. 10 | */ 11 | 12 | public class GroupingResult { 13 | public List> optFiles = new ArrayList<>(); 14 | public List badFiles = new ArrayList<>(); 15 | public List droppedFiles = new ArrayList<>(); 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/co/polarr/albumsdkdemo/utils/BenchmarkUtil.java: -------------------------------------------------------------------------------- 1 | package co.polarr.albumsdkdemo.utils; 2 | 3 | import android.app.ActivityManager; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.os.Debug; 7 | import android.util.Log; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | import static android.content.Context.ACTIVITY_SERVICE; 13 | 14 | 15 | public class BenchmarkUtil { 16 | private static final String TAG = "BenchmarkUtil"; 17 | private static final boolean ENABLE = !false; 18 | private static final int M = 1024 * 1024; 19 | private static final Runtime runtime = Runtime.getRuntime(); 20 | 21 | private static Map memMap = new HashMap<>(); 22 | private static Map memResultMap = new HashMap<>(); 23 | private static Map timeMap = new HashMap<>(); 24 | private static Map timeResultMap = new HashMap<>(); 25 | private static ActivityManager am; 26 | 27 | public static void init(Context context) { 28 | am = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE); 29 | } 30 | 31 | public static void MemStart(String name) { 32 | if (!ENABLE) return; 33 | 34 | memMap.put(name, currentMem()); 35 | } 36 | 37 | public static void MemEnd(String name) { 38 | if (!ENABLE) return; 39 | 40 | if (memMap.containsKey(name)) { 41 | long usedMem = currentMem() - memMap.get(name); 42 | 43 | memResultMap.put(name, usedMem); 44 | 45 | Log.i(TAG, "Mem of " + name + ": " + usedMem + "MB"); 46 | } 47 | } 48 | 49 | public static void TimeStart(String name) { 50 | if (!ENABLE) return; 51 | 52 | timeMap.put(name, System.currentTimeMillis()); 53 | } 54 | 55 | public static void TimeEnd(String name) { 56 | if (!ENABLE) return; 57 | 58 | if (timeMap.containsKey(name)) { 59 | long timeSpend = System.currentTimeMillis() - timeMap.get(name); 60 | timeMap.remove(name); 61 | timeResultMap.put(name, timeSpend); 62 | 63 | Log.i(TAG, "Time spend of " + name + ": " + timeSpend + "ms"); 64 | } 65 | } 66 | 67 | public static void TraceAllResult() { 68 | if (!ENABLE) return; 69 | 70 | for (String key : memResultMap.keySet()) { 71 | Log.i(TAG, "Mem of " + key + ": " + memResultMap.get(key) + "MB"); 72 | } 73 | for (String key : timeResultMap.keySet()) { 74 | Log.i(TAG, "Time spend of " + key + ": " + timeResultMap.get(key) + "ms"); 75 | } 76 | } 77 | 78 | private static long currentMem() { 79 | if(am != null) { 80 | ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo() ; 81 | am.getMemoryInfo(memoryInfo); 82 | 83 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 84 | return (memoryInfo.totalMem - memoryInfo.availMem) / M; 85 | } 86 | } 87 | return (runtime.totalMemory() - runtime.freeMemory()) / M; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/co/polarr/albumsdkdemo/utils/ExportUtil.java: -------------------------------------------------------------------------------- 1 | package co.polarr.albumsdkdemo.utils; 2 | 3 | import java.io.BufferedWriter; 4 | import java.io.File; 5 | import java.io.FileWriter; 6 | import java.io.IOException; 7 | import java.util.List; 8 | 9 | import co.polarr.processing.entities.ResultItem; 10 | 11 | /** 12 | * Created by Colin on 2017/11/29. 13 | * output rating results to an csv file 14 | */ 15 | 16 | public class ExportUtil { 17 | public static void ExportToCsv(List results, File targetFile) { 18 | try { 19 | StringBuilder sb = new StringBuilder(); 20 | sb.append("File Name,Clarity,Exposure,Colorfulness,Emotion,Overall\n"); 21 | 22 | for (ResultItem result : results) { 23 | if (result.filePath != null) { 24 | sb.append(new File(result.filePath).getName()); 25 | } 26 | sb.append(","); 27 | 28 | if (result.features.containsKey("metric_clarity")) { 29 | sb.append(result.features.get("metric_clarity")); 30 | } 31 | sb.append(","); 32 | 33 | if (result.features.containsKey("metric_exposure")) { 34 | sb.append(result.features.get("metric_exposure")); 35 | } 36 | sb.append(","); 37 | 38 | 39 | if (result.features.containsKey("metric_colorfulness")) { 40 | sb.append(result.features.get("metric_colorfulness")); 41 | } 42 | sb.append(","); 43 | 44 | 45 | if (result.features.containsKey("metric_emotion")) { 46 | float emotion = (float) result.features.get("metric_emotion"); 47 | if (emotion > 0) { 48 | sb.append(emotion); 49 | } 50 | } 51 | sb.append(","); 52 | 53 | 54 | if (result.features.containsKey("rating_all")) { 55 | sb.append(result.features.get("rating_all")); 56 | } 57 | 58 | sb.append("\n"); 59 | } 60 | 61 | BufferedWriter bwr = new BufferedWriter(new FileWriter(targetFile)); 62 | bwr.write(sb.toString()); 63 | bwr.flush(); 64 | bwr.close(); 65 | } catch (IOException e) { 66 | e.printStackTrace(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/co/polarr/albumsdkdemo/utils/FileUtils.java: -------------------------------------------------------------------------------- 1 | package co.polarr.albumsdkdemo.utils; 2 | 3 | import android.content.ContentUris; 4 | import android.content.Context; 5 | import android.database.Cursor; 6 | import android.graphics.Bitmap; 7 | import android.graphics.BitmapFactory; 8 | import android.net.Uri; 9 | import android.os.Build; 10 | import android.os.Environment; 11 | import android.provider.DocumentsContract; 12 | import android.provider.MediaStore; 13 | import androidx.annotation.RawRes; 14 | 15 | import java.io.ByteArrayOutputStream; 16 | import java.io.File; 17 | import java.io.FileInputStream; 18 | import java.io.FileOutputStream; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.io.OutputStream; 22 | import java.nio.channels.FileChannel; 23 | import java.util.Comparator; 24 | import java.util.List; 25 | 26 | 27 | /** 28 | * Created by dinqe on 09/04/16. 29 | */ 30 | public class FileUtils { 31 | 32 | public static void moveFile(File src, File dst) throws IOException { 33 | FileChannel outputChannel = null; 34 | FileChannel inputChannel = null; 35 | try { 36 | outputChannel = new FileOutputStream(dst).getChannel(); 37 | inputChannel = new FileInputStream(src).getChannel(); 38 | inputChannel.transferTo(0, inputChannel.size(), outputChannel); 39 | inputChannel.close(); 40 | src.delete(); 41 | } finally { 42 | if (inputChannel != null) inputChannel.close(); 43 | if (outputChannel != null) outputChannel.close(); 44 | } 45 | 46 | } 47 | 48 | public static void copyFile(File sourceFile, File destFile) throws IOException { 49 | 50 | if (!destFile.exists()) { 51 | destFile.createNewFile(); 52 | } 53 | 54 | FileChannel source = null; 55 | FileChannel destination = null; 56 | 57 | try { 58 | source = new FileInputStream(sourceFile).getChannel(); 59 | destination = new FileOutputStream(destFile).getChannel(); 60 | destination.transferFrom(source, 0, source.size()); 61 | } finally { 62 | if (source != null) { 63 | source.close(); 64 | } 65 | if (destination != null) { 66 | destination.close(); 67 | } 68 | } 69 | } 70 | 71 | public static void copyFileOrPath(File sourceFile, File destFile) throws IOException { 72 | if (sourceFile == null || !sourceFile.exists()) { 73 | return; 74 | } 75 | 76 | if (sourceFile.isDirectory()) { 77 | if (destFile.exists() && !destFile.isDirectory()) { 78 | destFile.delete(); 79 | } 80 | if (!destFile.exists()) { 81 | destFile.mkdir(); 82 | } 83 | 84 | for (File file : sourceFile.listFiles()) { 85 | copyFileOrPath(file, new File(destFile.getPath() + "/" + file.getName())); 86 | } 87 | } else { 88 | copyFile(sourceFile, destFile); 89 | } 90 | } 91 | 92 | public static byte[] readBytesFromStream(InputStream inputStream) throws IOException { 93 | if (inputStream == null) { 94 | return null; 95 | } 96 | ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream(); 97 | byte[] buffer = new byte[4096]; 98 | 99 | int len = 0; 100 | while ((len = inputStream.read(buffer)) != -1) { 101 | byteBuffer.write(buffer, 0, len); 102 | } 103 | 104 | byte[] data = byteBuffer.toByteArray(); 105 | 106 | byteBuffer.close(); 107 | 108 | return data; 109 | } 110 | 111 | public static byte[] readBytesFromFile(File file) throws IOException { 112 | return readBytesFromStream(new FileInputStream(file)); 113 | } 114 | 115 | public static boolean writeBytesToFile(File file, byte[] bytes) throws IOException { 116 | if (file == null || bytes == null) { 117 | return false; 118 | } 119 | 120 | OutputStream outputStream = new FileOutputStream(file); 121 | outputStream.write(bytes); 122 | 123 | return true; 124 | } 125 | 126 | public static Uri getDownloadableMediaUri(Context context, Uri uri) { 127 | InputStream is = null; 128 | try { 129 | is = context.getContentResolver().openInputStream(uri); 130 | Bitmap bmp = BitmapFactory.decodeStream(is); 131 | return writeToTempImageAndGetPathUri(context, bmp); 132 | } catch (Exception e) { 133 | e.printStackTrace(); 134 | } finally { 135 | try { 136 | is.close(); 137 | } catch (IOException e) { 138 | e.printStackTrace(); 139 | } 140 | } 141 | return null; 142 | } 143 | 144 | public static Uri writeToTempImageAndGetPathUri(Context inContext, Bitmap inImage) { 145 | ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 146 | inImage.compress(Bitmap.CompressFormat.JPEG, 100, bytes); 147 | String path = MediaStore.Images.Media.insertImage(inContext.getContentResolver(), inImage, "Title", null); 148 | return Uri.parse(path); 149 | } 150 | 151 | /** 152 | * @param uri The Uri to check. 153 | * @return Whether the Uri authority is ExternalStorageProvider. 154 | * @author paulburke 155 | */ 156 | public static boolean isExternalStorageDocument(Uri uri) { 157 | return "com.android.externalstorage.documents".equals(uri.getAuthority()); 158 | } 159 | 160 | /** 161 | * @param uri The Uri to check. 162 | * @return Whether the Uri authority is DownloadsProvider. 163 | * @author paulburke 164 | */ 165 | public static boolean isDownloadsDocument(Uri uri) { 166 | return "com.android.providers.downloads.documents".equals(uri.getAuthority()); 167 | } 168 | 169 | /** 170 | * @param uri The Uri to check. 171 | * @return Whether the Uri authority is MediaProvider. 172 | * @author paulburke 173 | */ 174 | public static boolean isMediaDocument(Uri uri) { 175 | return "com.android.providers.media.documents".equals(uri.getAuthority()); 176 | } 177 | 178 | /** 179 | * @param uri The Uri to check. 180 | * @return Whether the Uri authority is Google Drive. 181 | * @author boruiwang 182 | */ 183 | public static boolean isGoogleDriveDocument(Uri uri) { 184 | return "com.google.android.apps.docs.storage".equals(uri.getAuthority()); 185 | } 186 | 187 | /** 188 | * @param uri The Uri to check. 189 | * @return Whether the Uri authority is Google Photos. 190 | */ 191 | public static boolean isGooglePhotosUri(Uri uri) { 192 | return "com.google.android.apps.photos.contentprovider".equals(uri.getAuthority()); 193 | } 194 | 195 | /** 196 | * Get a file path from a Uri. This will get the the path for Storage Access 197 | * Framework Documents, as well as the _data field for the MediaStore and 198 | * other file-based ContentProviders.
199 | *
200 | * Callers should check whether the path is local before assuming it 201 | * represents a local file. 202 | * 203 | * @param context The context. 204 | * @param uri The Uri to query. 205 | * @author paulburke 206 | */ 207 | public static String getPath(final Context context, final Uri uri) { 208 | 209 | final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; 210 | 211 | // DocumentProvider 212 | if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { 213 | // ExternalStorageProvider 214 | if (isExternalStorageDocument(uri)) { 215 | final String docId = DocumentsContract.getDocumentId(uri); 216 | final String[] split = docId.split(":"); 217 | final String type = split[0]; 218 | if ("primary".equalsIgnoreCase(type)) { 219 | return Environment.getExternalStorageDirectory() + "/" + split[1]; 220 | } else { 221 | // this only works for Marshmallow, split[0] is the id of the sd card (random) 222 | return "/storage/" + type + "/" + split[1]; 223 | } 224 | } 225 | // DownloadsProvider 226 | if (isDownloadsDocument(uri)) { 227 | 228 | final String id = DocumentsContract.getDocumentId(uri); 229 | final Uri contentUri = ContentUris.withAppendedId( 230 | Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); 231 | 232 | return getDataColumn(context, contentUri, null, null); 233 | } 234 | // MediaProvider 235 | else if (isMediaDocument(uri)) { 236 | final String docId = DocumentsContract.getDocumentId(uri); 237 | final String[] split = docId.split(":"); 238 | final String type = split[0]; 239 | 240 | Uri contentUri = null; 241 | if ("image".equals(type)) { 242 | contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 243 | } else if ("video".equals(type)) { 244 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; 245 | } else if ("audio".equals(type)) { 246 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 247 | } 248 | 249 | final String selection = "_id=?"; 250 | final String[] selectionArgs = new String[]{ 251 | split[1] 252 | }; 253 | 254 | return getDataColumn(context, contentUri, selection, selectionArgs); 255 | } else if (isGoogleDriveDocument(uri)) { 256 | return getPath(context, getDownloadableMediaUri(context, uri)); 257 | } 258 | } 259 | // MediaStore (and general) 260 | else if ("content".equalsIgnoreCase(uri.getScheme())) { 261 | String path = getDataColumn(context, uri, null, null); 262 | if (path == null) { 263 | // this is likely to be google photos uri, but the uri is changing too frequently 264 | // so we're using null to check against it 265 | return getPath(context, getDownloadableMediaUri(context, uri)); 266 | } else { 267 | return path; 268 | } 269 | } 270 | // File 271 | else if ("file".equalsIgnoreCase(uri.getScheme())) { 272 | return uri.getPath(); 273 | } 274 | 275 | return null; 276 | } 277 | 278 | /** 279 | * Get the value of the data column for this Uri. This is useful for 280 | * MediaStore Uris, and other file-based ContentProviders. 281 | * 282 | * @param context The context. 283 | * @param uri The Uri to query. 284 | * @param selection (Optional) Filter used in the query. 285 | * @param selectionArgs (Optional) Selection arguments used in the query. 286 | * @return The value of the _data column, which is typically a file path. 287 | * @author paulburke 288 | */ 289 | public static String getDataColumn(Context context, Uri uri, String selection, 290 | String[] selectionArgs) { 291 | 292 | Cursor cursor = null; 293 | final String column = "_data"; 294 | final String[] projection = { 295 | column 296 | }; 297 | 298 | try { 299 | cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, 300 | null); 301 | if (cursor != null && cursor.moveToFirst()) { 302 | 303 | final int column_index = cursor.getColumnIndexOrThrow(column); 304 | return cursor.getString(column_index); 305 | } 306 | } finally { 307 | if (cursor != null) 308 | cursor.close(); 309 | } 310 | return null; 311 | } 312 | 313 | /** 314 | * get all file in a path with extensions 315 | * 316 | * @param files file list to store 317 | * @param filePath directory path 318 | * @param exts extensions 319 | * @param deep max deep for search 320 | */ 321 | public static void getFilesByExts(List files, String filePath, String[] exts, int deep) { 322 | if (deep == 0) { 323 | return; 324 | } 325 | File f = new File(filePath); 326 | if (!f.exists() || !f.isDirectory()) { 327 | return; 328 | } 329 | 330 | File[] subFiles = f.listFiles(); 331 | for (File subFile : subFiles) { 332 | for (String ext : exts) { 333 | if (subFile.isFile() && subFile.getName().toLowerCase().endsWith("." + ext.toLowerCase())) { 334 | files.add(subFile.getAbsolutePath()); 335 | } else if (subFile.isDirectory()) { 336 | getFilesByExts(files, subFile.getAbsolutePath(), exts, deep - 1); 337 | } 338 | } 339 | } 340 | } 341 | 342 | /** 343 | * Copy raw file to target path 344 | */ 345 | public static final void copyFileFromRawToOthers(final Context context, @RawRes int id, final String targetPath) { 346 | InputStream in = context.getResources().openRawResource(id); 347 | FileOutputStream out = null; 348 | try { 349 | out = new FileOutputStream(targetPath); 350 | byte[] buff = new byte[1024]; 351 | int read = 0; 352 | while ((read = in.read(buff)) > 0) { 353 | out.write(buff, 0, read); 354 | } 355 | } catch (Exception e) { 356 | e.printStackTrace(); 357 | } finally { 358 | try { 359 | if (in != null) { 360 | in.close(); 361 | } 362 | if (out != null) { 363 | out.close(); 364 | } 365 | } catch (IOException e) { 366 | e.printStackTrace(); 367 | } 368 | } 369 | } 370 | 371 | /** 372 | * file comparator by modify date 373 | */ 374 | public static class FileDateComparator implements Comparator { 375 | 376 | @Override 377 | public int compare(String lhs, String rhs) { 378 | int compareResult = 0; 379 | File fileLHS = new File(lhs); 380 | File fileRHS = new File(rhs); 381 | 382 | if (fileLHS.lastModified() < fileRHS.lastModified()) { 383 | compareResult = 1; 384 | } else if (fileLHS.lastModified() > fileRHS.lastModified()) { 385 | compareResult = -1; 386 | } 387 | 388 | return compareResult; 389 | } 390 | } 391 | } 392 | 393 | 394 | 395 | -------------------------------------------------------------------------------- /app/src/main/java/co/polarr/albumsdkdemo/utils/ImageRenderUtil.java: -------------------------------------------------------------------------------- 1 | package co.polarr.albumsdkdemo.utils; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.os.Handler; 6 | import android.os.Looper; 7 | import android.view.View; 8 | import android.view.animation.AlphaAnimation; 9 | import android.widget.ImageView; 10 | 11 | import java.util.concurrent.LinkedBlockingQueue; 12 | import java.util.concurrent.ThreadPoolExecutor; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | import co.polarr.albumsdkdemo.R; 16 | 17 | /** 18 | * Created by Colin on 2017/3/21. 19 | * render bitmap for imageviews 20 | */ 21 | 22 | public class ImageRenderUtil { 23 | private static Handler mainHandler; 24 | 25 | private synchronized static Handler getMainHandler() { 26 | if (mainHandler == null) { 27 | synchronized (ImageRenderUtil.class) { 28 | if (mainHandler == null) { 29 | mainHandler = new Handler(Looper.getMainLooper()); 30 | } 31 | } 32 | } 33 | 34 | return mainHandler; 35 | } 36 | 37 | /** 38 | * render thread pool 4 core threads 39 | */ 40 | private static final ThreadPoolExecutor renderPool = new ThreadPoolExecutor(4, 4, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue()); 41 | 42 | static class ImageRender { 43 | /** 44 | * local file path 45 | */ 46 | private String filePath; 47 | private Context context; 48 | /** 49 | * target max width 50 | */ 51 | public int width; 52 | /** 53 | * target max height 54 | */ 55 | public int height; 56 | /** 57 | * image view to be set 58 | */ 59 | private ImageView imageView; 60 | /** 61 | * temp bitmap object 62 | */ 63 | private Bitmap bitmap; 64 | 65 | /** 66 | * decode task 67 | */ 68 | private Runnable decodeRunnable; 69 | /** 70 | * render task in main thread 71 | */ 72 | private Runnable setRunnable; 73 | /** 74 | * stop render mark 75 | */ 76 | private boolean isCancel = false; 77 | public boolean needRecyle; 78 | 79 | /** 80 | * start working 81 | */ 82 | private void load() { 83 | decodeRunnable = new Runnable() { 84 | @Override 85 | public void run() { 86 | // do decode 87 | if (filePath != null) { 88 | bitmap = ImageUtil.decodeThumbBitmapForFile(filePath, width, height); 89 | } 90 | 91 | // check if cancel 92 | if (isCancel && bitmap != null && !bitmap.isRecycled()) { 93 | // bitmap.recycle(); 94 | } 95 | 96 | setRunnable = new Runnable() { 97 | @Override 98 | public void run() { 99 | // check if cancel 100 | if (isCancel && bitmap != null && !bitmap.isRecycled()) { 101 | // bitmap.recycle(); 102 | } 103 | 104 | /** do set image bitmap */ 105 | if (imageView != null && bitmap != null && !bitmap.isRecycled()) { 106 | imageView.setImageBitmap(bitmap); 107 | 108 | //appear animation 109 | AlphaAnimation showAnimation = new AlphaAnimation(0.0f, 1.0f); 110 | showAnimation.setDuration(500); 111 | showAnimation.setFillAfter(true); 112 | imageView.startAnimation(showAnimation); 113 | 114 | imageView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { 115 | @Override 116 | public void onViewAttachedToWindow(View v) { 117 | 118 | } 119 | 120 | @Override 121 | public void onViewDetachedFromWindow(View v) { 122 | if (needRecyle) { 123 | /** recyle bitmap if it detached from screen */ 124 | if (bitmap != null && !bitmap.isRecycled()) { 125 | // bitmap.recycle(); 126 | } 127 | } 128 | } 129 | }); 130 | } 131 | } 132 | }; 133 | getMainHandler().post(setRunnable); 134 | } 135 | }; 136 | /** real run the task*/ 137 | renderPool.execute(decodeRunnable); 138 | } 139 | 140 | /** 141 | * cancel task, will remove from queue and recyle 142 | */ 143 | public void cancel() { 144 | isCancel = true; 145 | if (decodeRunnable != null) { 146 | renderPool.remove(decodeRunnable); 147 | decodeRunnable = null; 148 | } 149 | if (setRunnable != null) { 150 | getMainHandler().removeCallbacks(setRunnable); 151 | setRunnable = null; 152 | } 153 | } 154 | } 155 | 156 | /** 157 | * Builder structure 158 | */ 159 | public static class Builder { 160 | private String filePath; 161 | private int width; 162 | private int height; 163 | private Context context; 164 | private boolean needRecyle = true; 165 | 166 | /** 167 | * image file path 168 | */ 169 | public Builder setPath(String filePath) { 170 | this.filePath = filePath; 171 | 172 | return this; 173 | } 174 | 175 | public Builder setNeedRecyle(boolean needRecyle) { 176 | this.needRecyle = needRecyle; 177 | 178 | return this; 179 | } 180 | 181 | /** 182 | * target image size 183 | */ 184 | public Builder setSize(int width, int height) { 185 | this.width = width; 186 | this.height = height; 187 | 188 | return this; 189 | } 190 | 191 | /** 192 | * instance call 193 | */ 194 | private ImageRender build() { 195 | ImageRender imageRender = new ImageRender(); 196 | imageRender.filePath = filePath; 197 | imageRender.context = context; 198 | imageRender.width = width; 199 | imageRender.height = height; 200 | imageRender.needRecyle = needRecyle; 201 | 202 | return imageRender; 203 | } 204 | 205 | /** 206 | * do image load 207 | */ 208 | public ImageRender into(ImageView imageView) { 209 | ImageRender render = (ImageRender) imageView.getTag(R.id.image_render_tag); 210 | if (render != null) { 211 | render.cancel(); 212 | } 213 | imageView.setImageBitmap(null); 214 | render = build(); 215 | render.imageView = imageView; 216 | imageView.setTag(R.id.image_render_tag, render); 217 | render.load(); 218 | 219 | return render; 220 | } 221 | } 222 | 223 | /** 224 | * create an instance easily 225 | */ 226 | public static Builder load() { 227 | return new Builder(); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /app/src/main/java/co/polarr/albumsdkdemo/utils/ImageUtil.java: -------------------------------------------------------------------------------- 1 | package co.polarr.albumsdkdemo.utils; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.BitmapFactory; 5 | import android.graphics.Matrix; 6 | import android.media.ExifInterface; 7 | 8 | import java.io.File; 9 | import java.text.ParseException; 10 | import java.text.SimpleDateFormat; 11 | import java.util.Locale; 12 | 13 | /** 14 | * Created by Colin on 2017/11/1. 15 | */ 16 | 17 | public class ImageUtil { 18 | // decode a thumb bitmap for a specific size 19 | public static Bitmap decodeThumbBitmapForFile(String path, int viewWidth, int viewHeight) { 20 | try { 21 | int degree = 0; 22 | ExifInterface exif; 23 | try { 24 | exif = new ExifInterface(path); 25 | } catch (Exception e) { 26 | exif = null; 27 | } 28 | if (exif != null) { 29 | // get the degree of image file 30 | int ori = exif 31 | .getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); 32 | // compute the rotation of file. 33 | switch (ori) { 34 | case ExifInterface.ORIENTATION_ROTATE_90: 35 | degree = 90; 36 | break; 37 | case ExifInterface.ORIENTATION_ROTATE_180: 38 | degree = 180; 39 | break; 40 | case ExifInterface.ORIENTATION_ROTATE_270: 41 | degree = 270; 42 | break; 43 | default: 44 | degree = 0; 45 | break; 46 | } 47 | } 48 | 49 | BitmapFactory.Options options = new BitmapFactory.Options(); 50 | options.inJustDecodeBounds = true; 51 | BitmapFactory.decodeFile(path, options); 52 | //not a bitmap file 53 | if (options.outHeight < 0) { 54 | return null; 55 | } 56 | options.inSampleSize = computeScale(options, viewWidth, viewHeight); 57 | options.inJustDecodeBounds = false; 58 | options.inPreferredConfig = Bitmap.Config.ARGB_8888; 59 | Bitmap bitmap = BitmapFactory.decodeFile(path, options); 60 | 61 | return getRotatedImage(bitmap, degree); 62 | } catch (Exception e) { 63 | e.printStackTrace(); 64 | } 65 | return null; 66 | } 67 | 68 | public static BitmapFactory.Options decodeImageSize(String path) { 69 | BitmapFactory.Options options = new BitmapFactory.Options(); 70 | options.inJustDecodeBounds = true; 71 | BitmapFactory.decodeFile(path, options); 72 | return options; 73 | } 74 | 75 | public static Bitmap getScaledBitmap(String imgPath, int destWidth, int destHeight) { 76 | int degree = 0; 77 | ExifInterface exif; 78 | try { 79 | exif = new ExifInterface(imgPath); 80 | } catch (Exception e) { 81 | exif = null; 82 | } 83 | if (exif != null) { 84 | // get the degree of image file 85 | int ori = exif 86 | .getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); 87 | // compute the rotation of file. 88 | switch (ori) { 89 | case ExifInterface.ORIENTATION_ROTATE_90: 90 | degree = 90; 91 | break; 92 | case ExifInterface.ORIENTATION_ROTATE_180: 93 | degree = 180; 94 | break; 95 | case ExifInterface.ORIENTATION_ROTATE_270: 96 | degree = 270; 97 | break; 98 | default: 99 | degree = 0; 100 | break; 101 | } 102 | } 103 | 104 | BitmapFactory.Options options = new BitmapFactory.Options(); 105 | options.inJustDecodeBounds = true; 106 | BitmapFactory.decodeFile(imgPath, options); 107 | //not a bitmap file 108 | if (options.outHeight < 0) { 109 | return null; 110 | } 111 | int inSampleSize = computeScale(options, destWidth, destHeight); 112 | options.inSampleSize = Math.max(1, inSampleSize - 1); 113 | options.inJustDecodeBounds = false; 114 | options.inPreferredConfig = Bitmap.Config.ARGB_8888; 115 | Bitmap bitmap = BitmapFactory.decodeFile(imgPath, options); 116 | Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, destWidth, destHeight, true); 117 | if (bitmap != scaledBitmap) { 118 | // bitmap.recycle(); 119 | } 120 | bitmap = scaledBitmap; 121 | 122 | return getRotatedImage(scaledBitmap, degree); 123 | } 124 | 125 | public static Bitmap getScaledFitBitmap(String imgPath, int destWidth, int destHeight) { 126 | int degree = 0; 127 | ExifInterface exif; 128 | try { 129 | exif = new ExifInterface(imgPath); 130 | } catch (Exception e) { 131 | exif = null; 132 | } 133 | if (exif != null) { 134 | // get the degree of image file 135 | int ori = exif 136 | .getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); 137 | // compute the rotation of file. 138 | switch (ori) { 139 | case ExifInterface.ORIENTATION_ROTATE_90: 140 | degree = 90; 141 | break; 142 | case ExifInterface.ORIENTATION_ROTATE_180: 143 | degree = 180; 144 | break; 145 | case ExifInterface.ORIENTATION_ROTATE_270: 146 | degree = 270; 147 | break; 148 | default: 149 | degree = 0; 150 | break; 151 | } 152 | } 153 | 154 | BitmapFactory.Options options = new BitmapFactory.Options(); 155 | options.inJustDecodeBounds = true; 156 | BitmapFactory.decodeFile(imgPath, options); 157 | //not a bitmap file 158 | if (options.outHeight < 0) { 159 | return null; 160 | } 161 | int inSampleSize = computeScale(options, destWidth, destHeight); 162 | options.inSampleSize = Math.max(1, inSampleSize - 1); 163 | options.inJustDecodeBounds = false; 164 | options.inPreferredConfig = Bitmap.Config.ARGB_8888; 165 | Bitmap bitmap = BitmapFactory.decodeFile(imgPath, options); 166 | float minScale = Math.min((float) destWidth / bitmap.getWidth(), (float) destHeight / bitmap.getHeight()); 167 | minScale = Math.min(1, minScale); 168 | int targetWidth = (int) (bitmap.getWidth() * minScale); 169 | int targetHeight = (int) (bitmap.getHeight() * minScale); 170 | Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, true); 171 | if (bitmap != scaledBitmap) { 172 | // bitmap.recycle(); 173 | } 174 | 175 | return getRotatedImage(scaledBitmap, degree); 176 | } 177 | 178 | // get the right rotation bitmap 179 | private static Bitmap getRotatedImage(Bitmap bitmap, int degrees) { 180 | if (bitmap != null && degrees != 0) { 181 | int w = bitmap.getWidth(); 182 | int h = bitmap.getHeight(); 183 | Matrix m = new Matrix(); 184 | m.postRotate(degrees, w / 2, h / 2); 185 | Bitmap bm = Bitmap.createBitmap(bitmap, 0, 0, w, h, m, true); 186 | if (bm != bitmap) { 187 | // bitmap.recycle(); 188 | } 189 | bitmap = bm; 190 | } 191 | 192 | return bitmap; 193 | } 194 | 195 | // compute the sample size for a specific size 196 | private static int computeScale(BitmapFactory.Options options, int viewWidth, int viewHeight) { 197 | float inSampleSize = 1; 198 | int bitmapWidth = options.outWidth; 199 | int bitmapHeight = options.outHeight; 200 | 201 | int maxViewSize = Math.max(viewHeight, viewWidth); 202 | 203 | if (bitmapWidth > maxViewSize || bitmapHeight > maxViewSize) { 204 | float widthScale = (float) bitmapWidth / (float) maxViewSize; 205 | float heightScale = (float) bitmapHeight / (float) maxViewSize; 206 | inSampleSize = Math.max(widthScale, heightScale); 207 | inSampleSize = (float) Math.max(Math.ceil(inSampleSize), 1); 208 | } 209 | return (int) inSampleSize; 210 | } 211 | 212 | private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US); 213 | 214 | public static long getPhotoCreationTime(File photo) { 215 | ExifInterface exif; 216 | try { 217 | exif = new ExifInterface(photo.getAbsolutePath()); 218 | String date = exif.getAttribute(ExifInterface.TAG_DATETIME); 219 | 220 | try { 221 | return simpleDateFormat.parse(date).getTime(); 222 | } catch (ParseException e) { 223 | e.printStackTrace(); 224 | } 225 | } catch (Exception e) { 226 | e.printStackTrace(); 227 | } 228 | 229 | return 0; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /app/src/main/java/co/polarr/albumsdkdemo/utils/MemoryCache.java: -------------------------------------------------------------------------------- 1 | package co.polarr.albumsdkdemo.utils; 2 | 3 | import android.util.LruCache; 4 | 5 | /** 6 | * Created by Colin on 2017/2/22. 7 | * Sample memory cache for transforming data between activities 8 | */ 9 | 10 | public class MemoryCache { 11 | private static LruCache cache = new LruCache<>(10); 12 | 13 | public static void put(String key, Object value) { 14 | if(value == null) { 15 | return; 16 | } 17 | cache.put(key, value); 18 | } 19 | 20 | public static Object get(String key) { 21 | return cache.get(key); 22 | } 23 | 24 | public static void remove(String key) { 25 | cache.remove(key); 26 | } 27 | 28 | public static Object pick(String key) { 29 | Object result = get(key); 30 | remove(key); 31 | return result; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/co/polarr/albumsdkdemo/utils/ScaledImageView.java: -------------------------------------------------------------------------------- 1 | package co.polarr.albumsdkdemo.utils; 2 | 3 | import android.content.Context; 4 | import android.graphics.Matrix; 5 | import android.graphics.RectF; 6 | import android.graphics.drawable.Drawable; 7 | import androidx.viewpager.widget.ViewPager; 8 | import android.util.AttributeSet; 9 | import android.view.GestureDetector; 10 | import android.view.MotionEvent; 11 | import android.view.ScaleGestureDetector; 12 | import android.view.View; 13 | import android.view.ViewConfiguration; 14 | import android.view.ViewTreeObserver; 15 | 16 | 17 | public class ScaledImageView extends androidx.appcompat.widget.AppCompatImageView implements ViewTreeObserver.OnGlobalLayoutListener, 18 | ScaleGestureDetector.OnScaleGestureListener, View.OnTouchListener { 19 | private boolean mOnce; 20 | 21 | /** 22 | * 初始化时缩放的值 23 | */ 24 | private float mInitScale; 25 | 26 | /** 27 | * 双击放大值到达的值 28 | */ 29 | private float mMidScale; 30 | 31 | /** 32 | * 放大的最大值 33 | */ 34 | private float mMaxScale; 35 | 36 | private Matrix mScaleMatrix; 37 | 38 | /** 39 | * 捕获用户多指触控时缩放的比例 40 | */ 41 | private ScaleGestureDetector mScaleGestureDetector; 42 | 43 | // **********自由移动的变量*********** 44 | /** 45 | * 记录上一次多点触控的数量 46 | */ 47 | private int mLastPointerCount; 48 | 49 | private float mLastX; 50 | private float mLastY; 51 | 52 | private int mTouchSlop; 53 | private boolean isCanDrag; 54 | 55 | private boolean isCheckLeftAndRight; 56 | private boolean isCheckTopAndBottom; 57 | 58 | // *********双击放大与缩小********* 59 | private GestureDetector mGestureDetector; 60 | 61 | private boolean isAutoScale; 62 | 63 | public ScaledImageView(Context context) { 64 | this(context, null); 65 | } 66 | 67 | public ScaledImageView(Context context, AttributeSet attrs) { 68 | this(context, attrs, 0); 69 | } 70 | 71 | public ScaledImageView(Context context, AttributeSet attrs, int defStyle) { 72 | super(context, attrs, defStyle); 73 | // init 74 | mScaleMatrix = new Matrix(); 75 | setScaleType(ScaleType.MATRIX); 76 | setOnTouchListener(this); 77 | mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 78 | mScaleGestureDetector = new ScaleGestureDetector(context, this); 79 | mGestureDetector = new GestureDetector(context, 80 | new GestureDetector.SimpleOnGestureListener() { 81 | @Override 82 | public boolean onDoubleTap(MotionEvent e) { 83 | 84 | if (isAutoScale) { 85 | return true; 86 | } 87 | 88 | float x = e.getX(); 89 | float y = e.getY(); 90 | 91 | if (getScale() < mMidScale) { 92 | postDelayed(new AutoScaleRunnable(mMidScale, x, y), 16); 93 | isAutoScale = true; 94 | } else { 95 | postDelayed(new AutoScaleRunnable(mInitScale, x, y), 16); 96 | isAutoScale = true; 97 | } 98 | return true; 99 | } 100 | }); 101 | } 102 | 103 | /** 104 | * 自动放大与缩小 105 | * 106 | * @author zhangyan@lzt.com.cn 107 | */ 108 | private class AutoScaleRunnable implements Runnable { 109 | /** 110 | * 缩放的目标值 111 | */ 112 | private float mTargetScale; 113 | // 缩放的中心点 114 | private float x; 115 | private float y; 116 | 117 | private final float BIGGER = 1.07f; 118 | private final float SMALL = 0.93f; 119 | 120 | private float tmpScale; 121 | 122 | /** 123 | * @param mTargetScale 124 | * @param x 125 | * @param y 126 | */ 127 | public AutoScaleRunnable(float mTargetScale, float x, float y) { 128 | this.mTargetScale = mTargetScale; 129 | this.x = x; 130 | this.y = y; 131 | 132 | if (getScale() < mTargetScale) { 133 | tmpScale = BIGGER; 134 | } 135 | if (getScale() > mTargetScale) { 136 | tmpScale = SMALL; 137 | } 138 | } 139 | 140 | @Override 141 | public void run() { 142 | //进行缩放 143 | mScaleMatrix.postScale(tmpScale, tmpScale, x, y); 144 | checkBorderAndCenterWhenScale(); 145 | setImageMatrix(mScaleMatrix); 146 | 147 | float currentScale = getScale(); 148 | 149 | if ((tmpScale > 1.0f && currentScale < mTargetScale) || (tmpScale < 1.0f && currentScale > mTargetScale)) { 150 | //这个方法是重新调用run()方法 151 | postDelayed(this, 16); 152 | } else { 153 | //设置为我们的目标值 154 | float scale = mTargetScale / currentScale; 155 | mScaleMatrix.postScale(scale, scale, x, y); 156 | checkBorderAndCenterWhenScale(); 157 | setImageMatrix(mScaleMatrix); 158 | 159 | isAutoScale = false; 160 | } 161 | } 162 | } 163 | 164 | /** 165 | * 获取ImageView加载完成的图片 166 | */ 167 | @Override 168 | public void onGlobalLayout() { 169 | if (!mOnce) { 170 | // 得到控件的宽和高 171 | int width = getWidth(); 172 | int height = getHeight(); 173 | 174 | // 得到我们的图片,以及宽和高 175 | Drawable drawable = getDrawable(); 176 | if (drawable == null) { 177 | return; 178 | } 179 | int dh = drawable.getIntrinsicHeight(); 180 | int dw = drawable.getIntrinsicWidth(); 181 | 182 | float scale = 1.0f; 183 | 184 | // 图片的宽度大于控件的宽度,图片的高度小于空间的高度,我们将其缩小 185 | if (dw > width && dh < height) { 186 | scale = width * 1.0f / dw; 187 | } 188 | 189 | // 图片的宽度小于控件的宽度,图片的高度大于空间的高度,我们将其缩小 190 | if (dh > height && dw < width) { 191 | scale = height * 1.0f / dh; 192 | } 193 | 194 | // 缩小值 195 | if (dw > width && dh > height) { 196 | scale = Math.min(width * 1.0f / dw, height * 1.0f / dh); 197 | } 198 | 199 | // 放大值 200 | if (dw < width && dh < height) { 201 | scale = Math.min(width * 1.0f / dw, height * 1.0f / dh); 202 | } 203 | 204 | /** 205 | * 得到了初始化时缩放的比例 206 | */ 207 | mInitScale = scale; 208 | mMaxScale = mInitScale * 4; 209 | mMidScale = mInitScale * 2; 210 | 211 | // 将图片移动至控件的中间 212 | int dx = getWidth() / 2 - dw / 2; 213 | int dy = getHeight() / 2 - dh / 2; 214 | 215 | mScaleMatrix.postTranslate(dx, dy); 216 | mScaleMatrix.postScale(mInitScale, mInitScale, width / 2, 217 | height / 2); 218 | setImageMatrix(mScaleMatrix); 219 | 220 | mOnce = true; 221 | } 222 | } 223 | 224 | /** 225 | * 注册OnGlobalLayoutListener这个接口 226 | */ 227 | @Override 228 | protected void onAttachedToWindow() { 229 | super.onAttachedToWindow(); 230 | getViewTreeObserver().addOnGlobalLayoutListener(this); 231 | } 232 | 233 | /** 234 | * 取消OnGlobalLayoutListener这个接口 235 | */ 236 | @SuppressWarnings("deprecation") 237 | @Override 238 | protected void onDetachedFromWindow() { 239 | super.onDetachedFromWindow(); 240 | getViewTreeObserver().removeGlobalOnLayoutListener(this); 241 | } 242 | 243 | /** 244 | * 获取当前图片的缩放值 245 | * 246 | * @return 247 | */ 248 | public float getScale() { 249 | float[] values = new float[9]; 250 | mScaleMatrix.getValues(values); 251 | return values[Matrix.MSCALE_X]; 252 | } 253 | 254 | // 缩放区间时initScale maxScale 255 | @Override 256 | public boolean onScale(ScaleGestureDetector detector) { 257 | float scale = getScale(); 258 | float scaleFactor = detector.getScaleFactor(); 259 | 260 | if (getDrawable() == null) { 261 | return true; 262 | } 263 | 264 | // 缩放范围的控制 265 | if ((scale < mMaxScale && scaleFactor > 1.0f) 266 | || (scale > mInitScale && scaleFactor < 1.0f)) { 267 | if (scale * scaleFactor < mInitScale) { 268 | scaleFactor = mInitScale / scale; 269 | } 270 | 271 | if (scale * scaleFactor > mMaxScale) { 272 | scale = mMaxScale / scale; 273 | } 274 | 275 | // 缩放 276 | mScaleMatrix.postScale(scaleFactor, scaleFactor, 277 | detector.getFocusX(), detector.getFocusY()); 278 | 279 | checkBorderAndCenterWhenScale(); 280 | 281 | setImageMatrix(mScaleMatrix); 282 | } 283 | 284 | return true; 285 | } 286 | 287 | /** 288 | * 获得图片放大缩小以后的宽和高,以及left,right,top,bottom 289 | * 290 | * @return 291 | */ 292 | private RectF getMatrixRectF() { 293 | Matrix matrix = mScaleMatrix; 294 | RectF rectF = new RectF(); 295 | Drawable d = getDrawable(); 296 | if (d != null) { 297 | rectF.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); 298 | matrix.mapRect(rectF); 299 | } 300 | return rectF; 301 | } 302 | 303 | /** 304 | * 在缩放的时候进行边界以及我们的位置的控制 305 | */ 306 | private void checkBorderAndCenterWhenScale() { 307 | RectF rectF = getMatrixRectF(); 308 | float deltaX = 0; 309 | float deltaY = 0; 310 | 311 | int width = getWidth(); 312 | int height = getHeight(); 313 | 314 | // 缩放时进行边界检测,防止出现白边 315 | if (rectF.width() >= width) { 316 | if (rectF.left > 0) { 317 | deltaX = -rectF.left; 318 | } 319 | if (rectF.right < width) { 320 | deltaX = width - rectF.right; 321 | } 322 | } 323 | 324 | if (rectF.height() >= height) { 325 | if (rectF.top > 0) { 326 | deltaY = -rectF.top; 327 | } 328 | if (rectF.bottom < height) { 329 | deltaY = height - rectF.bottom; 330 | } 331 | } 332 | 333 | /** 334 | * 如果宽度或高度小于空间的宽或者高,则让其居中 335 | */ 336 | if (rectF.width() < width) { 337 | deltaX = width / 2f - rectF.right + rectF.width() / 2f; 338 | } 339 | 340 | if (rectF.height() < height) { 341 | deltaY = height / 2f - rectF.bottom + rectF.height() / 2f; 342 | } 343 | 344 | mScaleMatrix.postTranslate(deltaX, deltaY); 345 | } 346 | 347 | @Override 348 | public boolean onScaleBegin(ScaleGestureDetector detector) { 349 | 350 | return true; 351 | } 352 | 353 | @Override 354 | public void onScaleEnd(ScaleGestureDetector detector) { 355 | 356 | } 357 | 358 | @Override 359 | public boolean onTouch(View v, MotionEvent event) { 360 | 361 | if (mGestureDetector.onTouchEvent(event)) { 362 | return true; 363 | } 364 | 365 | mScaleGestureDetector.onTouchEvent(event); 366 | 367 | float x = 0; 368 | float y = 0; 369 | // 拿到多点触控的数量 370 | int pointerCount = event.getPointerCount(); 371 | for (int i = 0; i < pointerCount; i++) { 372 | x += event.getX(i); 373 | y += event.getY(i); 374 | } 375 | 376 | x /= pointerCount; 377 | y /= pointerCount; 378 | 379 | if (mLastPointerCount != pointerCount) { 380 | isCanDrag = false; 381 | mLastX = x; 382 | mLastY = y; 383 | } 384 | mLastPointerCount = pointerCount; 385 | RectF rectF = getMatrixRectF(); 386 | switch (event.getAction()) { 387 | 388 | case MotionEvent.ACTION_DOWN: 389 | if (rectF.width() > getWidth() + 0.01 || rectF.height() > getHeight() + 0.01) { 390 | if (getParent() instanceof ViewPager) 391 | getParent().requestDisallowInterceptTouchEvent(true); 392 | } 393 | break; 394 | 395 | case MotionEvent.ACTION_MOVE: 396 | if (rectF.width() > getWidth() + 0.01 || rectF.height() > getHeight() + 0.01) { 397 | if (getParent() instanceof ViewPager) 398 | getParent().requestDisallowInterceptTouchEvent(true); 399 | } 400 | float dx = x - mLastX; 401 | float dy = y - mLastY; 402 | 403 | if (!isCanDrag) { 404 | isCanDrag = isMoveAction(dx, dy); 405 | } 406 | 407 | if (isCanDrag) { 408 | if (getDrawable() != null) { 409 | isCheckLeftAndRight = isCheckTopAndBottom = true; 410 | // 如果宽度小于控件宽度,不允许横向移动 411 | if (rectF.width() < getWidth()) { 412 | isCheckLeftAndRight = false; 413 | dx = 0; 414 | } 415 | // 如果高度小于控件高度,不允许纵向移动 416 | if (rectF.height() < getHeight()) { 417 | isCheckTopAndBottom = false; 418 | dy = 0; 419 | } 420 | mScaleMatrix.postTranslate(dx, dy); 421 | 422 | checkBorderWhenTranslate(); 423 | 424 | setImageMatrix(mScaleMatrix); 425 | } 426 | } 427 | mLastX = x; 428 | mLastY = y; 429 | break; 430 | case MotionEvent.ACTION_UP: 431 | case MotionEvent.ACTION_CANCEL: 432 | mLastPointerCount = 0; 433 | break; 434 | 435 | default: 436 | break; 437 | } 438 | 439 | return true; 440 | } 441 | 442 | /** 443 | * 当移动时进行边界检查 444 | */ 445 | private void checkBorderWhenTranslate() { 446 | RectF rectF = getMatrixRectF(); 447 | float deltaX = 0; 448 | float deltaY = 0; 449 | 450 | int width = getWidth(); 451 | int heigth = getHeight(); 452 | 453 | if (rectF.top > 0 && isCheckTopAndBottom) { 454 | deltaY = -rectF.top; 455 | } 456 | if (rectF.bottom < heigth && isCheckTopAndBottom) { 457 | deltaY = heigth - rectF.bottom; 458 | } 459 | if (rectF.left > 0 && isCheckLeftAndRight) { 460 | deltaX = -rectF.left; 461 | } 462 | if (rectF.right < width && isCheckLeftAndRight) { 463 | deltaX = width - rectF.right; 464 | } 465 | mScaleMatrix.postTranslate(deltaX, deltaY); 466 | 467 | } 468 | 469 | /** 470 | * 判断是否是move 471 | * 472 | * @param dx 473 | * @param dy 474 | * @return 475 | */ 476 | private boolean isMoveAction(float dx, float dy) { 477 | return Math.sqrt(dx * dx + dy * dy) > mTouchSlop; 478 | } 479 | 480 | } 481 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_grouping.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 21 | 22 |