Compare commits
89 Commits
debugtemp-
...
gallery
| Author | SHA1 | Date | |
|---|---|---|---|
| 74ab06448f | |||
| 6cf5ac2034 | |||
| a2884122aa | |||
| 03ae37dc91 | |||
| aea9f1d745 | |||
| 0786d69ad6 | |||
| 1585ff7eed | |||
| 91b2b1b480 | |||
| bef3f3ce81 | |||
| c0da46e0fd | |||
| 72ca11a1af | |||
| 5decb2f8d9 | |||
| 29e7cfe985 | |||
| 6d521fefdb | |||
| f7932c134f | |||
| 93c59b0424 | |||
| fe248349df | |||
| 4790238343 | |||
| f144d91bb6 | |||
| 9c0e56462e | |||
| 4d977646e6 | |||
| 2d912abf92 | |||
| f764de4077 | |||
| 0788a52652 | |||
| ffea383a4e | |||
| 9c1e08a88b | |||
| abb28b766a | |||
| b6cd91c298 | |||
| 61c2bab83e | |||
| 5a700a0808 | |||
| dad179c15f | |||
| 1b62ff288f | |||
| 87ad6668a4 | |||
| 139083c22f | |||
| c49e68d7f1 | |||
| b035d461aa | |||
| 474ddcbb3b | |||
| a7e2646eca | |||
| a12577a369 | |||
| 990ca8e27c | |||
| 7f6cd8295c | |||
| ae3a0c6f64 | |||
| e07931fd3b | |||
| 8d62e7df21 | |||
| 1cca476acd | |||
| 31ea5c8fbb | |||
| e62a907378 | |||
| c85ba8324b | |||
| 3e49c33bc1 | |||
| 3aab93cc4d | |||
| 90d8330798 | |||
| b01482470a | |||
| 57c36b09ac | |||
| b5d68dfd1e | |||
| 262d35fb4e | |||
| 5b0bb599bb | |||
| 3955de100d | |||
| 7456db1729 | |||
| 226cbf43fe | |||
| 4a267d5606 | |||
| 4d0d8c4d59 | |||
| 9a43267b63 | |||
| c2618672bb | |||
| 6d9bd175f6 | |||
| ffbecaa31d | |||
| e26df437c5 | |||
| 248fd9d8d8 | |||
| 5b631710a9 | |||
| cda85feddd | |||
| ecad4a7913 | |||
| 2f14443624 | |||
| 21f38be35d | |||
| ab9ff33b72 | |||
| 5bc930e96d | |||
| 6c8867e15c | |||
| 610d3811db | |||
| 2d949eb5a3 | |||
| e6940805d9 | |||
| 1641424276 | |||
| 5d1cdff283 | |||
| da66cea1e5 | |||
| 5eb7441dc7 | |||
| 5f3168e17f | |||
| e3c4bab6c9 | |||
| 2af6427ca8 | |||
| b8c70bef98 | |||
| 7713d6c460 | |||
| 73c69bd665 | |||
| a076fe50cd |
205
README.md
@@ -1,104 +1,103 @@
|
|||||||
WinBoLL 源生态计划项目说明书
|
# WinBoLL 源生态计划项目说明书
|
||||||
|
|
||||||
|
## 一、项目概述
|
||||||
|
|
||||||
|
### 1. 核心定位
|
||||||
|
WinBoLL 手机源码计划,旨在通过核心项目 WinBoLL 构建手机端与服务器端的 Android 项目的开发源码生态。实现手机与服务器的源码的联合开发。
|
||||||
|
|
||||||
|
### 2. 仓库架构
|
||||||
|
#### **仓库类型:功能说明**
|
||||||
|
☆ 基础项目分支 WinBoLL:手机端安卓应用开发基础模板。
|
||||||
|
☆ 应用项目分支 APPBase、AES、PowerBell、Positions**:安卓应用单一管理系列项目。
|
||||||
|
☆ 源码汇总管理 OriginMaster**:各类分支源码合并存档,不适宜作为开发库使用。
|
||||||
|
|
||||||
|
### 3. 源码合并管理推送路线图
|
||||||
|
⚠️ **注意**:仅仅展示不同应用模块源码的综合管理路线。分支合并操作时,必须具备 Git 管理经验。
|
||||||
|
|
||||||
|
★ WinBoLL → APPBase → OriginMaster
|
||||||
|
★ WinBoLL → AES → OriginMaster
|
||||||
|
★ WinBoLL → PowerBell → OriginMaster
|
||||||
|
★ WinBoLL → Positions → OriginMaster
|
||||||
|
|
||||||
|
## 二、WinBoLL 项目核心信息
|
||||||
|
|
||||||
|
### 1. 项目简介
|
||||||
|
☆ WinBoLL 项目是为手机端开发Android 项目的需求而设计的项目。
|
||||||
|
|
||||||
|
### 2. 官方资源
|
||||||
|
#### ☆ 官方网站**:https://www.winboll.cc/
|
||||||
|
#### ☆ 源码地址:
|
||||||
|
★ Gitea:https://gitea.winboll.cc/Studio/WinBoLL.git
|
||||||
|
★ GitHub:https://github.com/ZhanGSKen/WinBoLL.git
|
||||||
|
★ 码云:https://gitee.com/zhangsken/winboll.git
|
||||||
|
|
||||||
|
## 三、应用编译环境检查问题
|
||||||
|
### 核心判断条件:
|
||||||
|
☆ WinBoLL 项目以文件夹 `"/sdcard/WinBoLLStudio/APKs"` 是否存在为判断环境编译输出条件,因为编译输出的APK文件需要一个可供保存的环境。
|
||||||
|
|
||||||
|
☆ 文件夹"/sdcard/WinBoLLStudio/APKs" 目录条件设置方法:
|
||||||
|
***Linux 服务器端方面***:建立 `/sdcard/WinBoLLStudio/APKs` 目录即可。
|
||||||
|
***手机开发端方面***:建立 `"/sdcard/WinBoLLStudio/APKs"` 目录(即 `"/storage/emulated/0/WinBoLLStudio/APKs"` 目录) 即可。
|
||||||
|
|
||||||
|
## 四、前置条件
|
||||||
|
|
||||||
|
### 1. WinBoLL APP 开发环境配置介绍
|
||||||
|
#### WinBoLL APK 编译输出内容包括:
|
||||||
|
☆ "/sdcard/WinBoLLStudio/APKs"` 目录内的所有应用分支的 APK 文件。
|
||||||
|
☆ "/sdcard/AppProjects/app.apk"文件。
|
||||||
|
#### WinBoLL APK 源码命名空间规范
|
||||||
|
☆ WinBoLL 项目使用 "cc.winboll.studio" 作为源码命名空间。在此命名空间下进行源码定义。
|
||||||
|
|
||||||
|
## 五、核心需求规划
|
||||||
|
|
||||||
|
### 1. WinBoLL 应用安全验证需求
|
||||||
|
#### ☆ 支持访问 https://console.winboll.cc/ 服务器以校验应用包签名与版本。
|
||||||
|
|
||||||
|
### 2. 手机端源码开发管理需求
|
||||||
|
#### ☆ 支持切换不同 WinBoLL 分支,以开发不同安卓应用。
|
||||||
|
|
||||||
|
## 六、编译与使用指南
|
||||||
|
|
||||||
|
### 1. 项目初始化(必须)
|
||||||
|
#### 1. 复制 `settings.gradle-demo` 为 `settings.gradle`。编辑 `settings.gradle` 文件内容,取消对应项目模块注释。
|
||||||
|
#### 2. 复制 `gradle.properties-androidx-demo` (Android X 项目) 或 `gradle.properties-android-demo` (基本 Android 项目) 为 `gradle.properties`。
|
||||||
|
#### 3. 复制(可选)`local.properties-demo` 为 `local.properties`,编辑 `local.properties` 文件内容,配置 Android SDK 目录。
|
||||||
|
#### 4. **签名设置**:
|
||||||
|
☆ **调试编译秘钥制作**:使用 Termux 应用终端,cd 进入 GenKeyStore 目录,运行 `bash gen_debug_keystore.sh` 脚本即可生成应用调试秘钥。
|
||||||
|
☆ **应用秘钥配置方法**:拷贝调试编译秘钥制作生成的 `appkey.jks` 与 `appkey.keystore` 文件到项目根目录即可。
|
||||||
|
|
||||||
|
## 七、应用编译命令介绍
|
||||||
|
|
||||||
|
### (1)类库型模块配置要点
|
||||||
|
#### 1. **优先修改配置文件**:优先修改应用测试项目(目录为 `"<WinBoLl根目录>/<类库测试应用>/"`)内 `build.properties` 文件,设置对应的类库项目名称:`libraryProject=<类库项目模块名>`。
|
||||||
|
#### 2. **编译优先启动步骤**:使用 Termux 应用,进入 `"<WinBoLl根目录>"`,运行 `$ bash .winboll/bashPublishAPKAddTag.sh <类库测试项目模块名>` 命令。运行后可生成测试项目与类库项目的编译参数文件 `build.properties`。生成的 `build.properties` 文件有两份,一份在测试项目模块的文件夹内,一份在类库项目本身的模块文件夹内。
|
||||||
|
#### 3. **最后类库编译发布步骤**:使用 Termux 应用,进入 `"<WinBoLl根目录>"`,运行 `$ bash .winboll/bashPublishLIBAddTag.sh <类库项目模块名>` 命令。运行后可发布至 WinBoLL Nexus Maven 库、本地 maven 目录或者是通用默认的 Gradle Maven 库。
|
||||||
|
|
||||||
|
### (2)单一应用型模块与类库测试型模块配置要点
|
||||||
|
#### ☆ APK 编译方法:
|
||||||
|
使用 Termux 应用,进入 `"<WinBoLl根目录>"`,运行 `$ bash .winboll/bashPublishAPKAddTag.sh <应用项目模块名>`。
|
||||||
|
#### ☆ 运行后的 APK 输出路径:
|
||||||
|
★ 默认路径 (`$ bash gradlew assembleBetaDebug` 任务):APK 在 `/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/debug/` 目录。
|
||||||
|
★ 默认路径 (`$ bash assembleStageRelease` 任务):APK 在 `/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/tag/` 目录。
|
||||||
|
★ 额外输出路径:(假设 `winboll.properties` 文件已配置 `ExtraAPKOutputPath` 属性) 输出至 `ExtraAPKOutputPath` 属性配置的目录下。
|
||||||
|
|
||||||
|
### (3)手机端应用调试命令介绍
|
||||||
|
#### ☆ Beta 渠道调试命令
|
||||||
|
$bash gradlew assembleBetaDebug
|
||||||
|
|
||||||
|
#### ☆ Stage 渠道调试命令
|
||||||
|
$bash gradlew assembleStageDebug
|
||||||
|
|
||||||
|
### (4)服务器端开发命令介绍
|
||||||
|
##### ☆ Stage 渠道应用发布命令为:
|
||||||
|
("<WinBoLl根目录>/settings.gradle"文件需要配置编译模块开启参数,拷贝 settings.gradle-demo 为 settings.gradle 文件取消对应的分支配置部分即可。)
|
||||||
|
$bash .winboll/bashPublishAPKAddTag.sh <应用项目模块名>
|
||||||
|
或者是
|
||||||
|
$bash gradlew assembleStageRelease
|
||||||
|
|
||||||
一、项目概述
|
|
||||||
|
## 八、WinBoLL 应用 APK 版本号命名规则
|
||||||
1. 核心定位
|
### ☆ Stage 渠道:
|
||||||
|
#### V<应用开发环境编号><应用功能变更号><应用调试阶段号> (示例: APPBase_15.7.0 )
|
||||||
【OriginMaster】WinBoLL 源生态计划,旨在通过核心项目 WinBoLL 联动系列开发库,构建手机端 Android 项目开发与多端编译同步的完整生态,实现手机与电脑的源码同步开发。
|
### ☆ Beta 渠道:
|
||||||
|
#### V<应用开发环境编号><应用功能变更号><应用调试阶段号>-beta<调试编译计数>_<调试编译时间(分钟+秒钟)> (示例: APPBase_15.9.6-beta8_5413 )
|
||||||
2. 仓库架构
|
|
||||||
|
|
||||||
仓库类型 包含仓库 功能说明
|
|
||||||
开发库 WinBoLL、APPBase、AES、PowerBell、Positions 核心开发依赖库,其中 WinBoLL 可作为应用开发的基础继承模板
|
|
||||||
分支汇总存档库 OriginMaster 仅用于汇总各开发库分支,不适宜作为开发库克隆使用,非应用开发基础库
|
|
||||||
|
|
||||||
3. 源码推送路径
|
|
||||||
|
|
||||||
- WinBoLL → APPBase → OriginMaster
|
|
||||||
- WinBoLL → AES → OriginMaster
|
|
||||||
- WinBoLL → PowerBell → OriginMaster
|
|
||||||
- WinBoLL → Positions → OriginMaster
|
|
||||||
|
|
||||||
二、WinBoLL APP 核心信息
|
|
||||||
|
|
||||||
1. 项目简介
|
|
||||||
|
|
||||||
WinBoLL Studio Android 应用开源项目,专注于手机端 Android 开发与多端编译同步。
|
|
||||||
|
|
||||||
2. 官方资源
|
|
||||||
|
|
||||||
- 官方网站:https://www.winboll.cc/
|
|
||||||
- 源码地址:
|
|
||||||
- Gitea:https://gitea.winboll.cc/Studio/WinBoLL.git
|
|
||||||
- GitHub:https://github.com/ZhanGSKen/WinBoLL.git
|
|
||||||
- 码云:https://gitee.com/zhangsken/winboll.git
|
|
||||||
- 托管类库源码:
|
|
||||||
- APPBase(jitpack.io):https://github.com/ZhanGSKen/APPBase.git
|
|
||||||
- AES(jitpack.io):https://github.com/ZhanGSKen/AES.git
|
|
||||||
|
|
||||||
三、通用特征文件夹前置(/sdcard)
|
|
||||||
|
|
||||||
- Linux 系统文件夹直接使用 /sdcard 。
|
|
||||||
- 手机 SD 卡存储( /storage/emulated/0 )挂载的别名也可为 /sdcard 。
|
|
||||||
|
|
||||||
四、前置条件
|
|
||||||
|
|
||||||
1. WinBoLL-APP 配置
|
|
||||||
|
|
||||||
- APK 编译输出目录: /sdcard/WinBoLLStudio/APKs/ ,以及 /sdcard/AppProjects/ (命名为 app.apk )
|
|
||||||
- 签名与命名空间:支持应用签名验证定制化,与衍生 APP 共享 cc.winboll.studio 命名空间
|
|
||||||
|
|
||||||
五、核心需求规划
|
|
||||||
|
|
||||||
1. 主机端需求
|
|
||||||
|
|
||||||
- 支持 winboll.cc 域名的用户注册登录服务
|
|
||||||
- 支持 https://console.winboll.cc/api 访问
|
|
||||||
|
|
||||||
2. APP 端需求
|
|
||||||
|
|
||||||
- 实现手机端 Android 应用开发与管理功能
|
|
||||||
|
|
||||||
六、编译与使用指南
|
|
||||||
|
|
||||||
1. 项目初始化(必须)
|
|
||||||
|
|
||||||
1. 复制 settings.gradle-demo 为 settings.gradle ,取消对应项目模块注释
|
|
||||||
2. 复制 gradle.properties-androidx-demo 或 gradle.properties-android-demo 为 gradle.properties
|
|
||||||
3. (可选)复制 local.properties-demo 为 local.properties ,配置 Android SDK 目录
|
|
||||||
4. 签名设置:
|
|
||||||
- 调试编译:进入 GenKeyStore 目录执行 bash gen_debug_keystore.sh
|
|
||||||
- 非必须:clone keystore 模块,拷贝 appkey.jks 与 appkey.keystore 到项目根目录
|
|
||||||
|
|
||||||
2. 编译命令
|
|
||||||
|
|
||||||
(1)类库型项目
|
|
||||||
|
|
||||||
1. 修改测试项目 build.properties ,设置 libraryProject=<类库项目模块名>
|
|
||||||
2. 编译测试项目: bash .winboll/bashPublishAPKAddTag.sh <应用项目模块名>
|
|
||||||
3. 编译类库项目: bash .winboll/bashPublishLIBAddTag.sh <类库项目模块名> (发布至 WinBoLL Nexus Maven 库)
|
|
||||||
|
|
||||||
(2)应用型项目
|
|
||||||
|
|
||||||
- 编译命令: bash .winboll/bashPublishAPKAddTag.sh <应用项目模块名>
|
|
||||||
|
|
||||||
(3)调试编译
|
|
||||||
|
|
||||||
- Beta 调试: bash gradlew assembleBetaDebug
|
|
||||||
- Stage 调试: bash gradlew assembleStageDebug
|
|
||||||
|
|
||||||
(4)发布编译
|
|
||||||
|
|
||||||
- Stage 发布:bash .winboll/bashPublishAPKAddTag.sh <应用项目模块名>
|
|
||||||
或者执行 bash gradlew assembleStageRelease
|
|
||||||
|
|
||||||
3. 编译输出路径
|
|
||||||
|
|
||||||
- 默认路径(assembleBetaDebug任务): /sdcard/WinBoLLStudio/APKs/<项目根目录名称>/debug/
|
|
||||||
- 默认路径(assembleStageRelease任务): /sdcard/WinBoLLStudio/APKs/<项目根目录名称>/tag/
|
|
||||||
- 额外路径:若 winboll.properties 配置 ExtraAPKOutputPath ,APK 同步拷贝至该ExtraAPKOutputPath路径
|
|
||||||
|
|
||||||
4. 版本号命名规则
|
|
||||||
|
|
||||||
- Stage 渠道: V<应用开发环境编号><应用功能变更号><应用调试阶段号> (示例: APPBase_15.7.0 )
|
|
||||||
- Beta 渠道: V<应用开发环境编号><应用功能变更号><应用调试阶段号>-beta<调试编译计数>_<调试编译时间(分钟+秒钟)> (示例: APPBase_15.9.6-beta8_5413 )
|
|
||||||
|
|||||||
44
build.gradle
@@ -1,6 +1,15 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/public/' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/google/' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
|
||||||
|
maven { url 'https://dl.bintray.com/ppartisan/maven/' }
|
||||||
|
maven { url "https://clojars.org/repo/" }
|
||||||
|
maven { url "https://jitpack.io" }
|
||||||
|
mavenCentral()
|
||||||
|
google()
|
||||||
|
|
||||||
mavenLocal {
|
mavenLocal {
|
||||||
// 设置本地Maven仓库路径
|
// 设置本地Maven仓库路径
|
||||||
url 'file:///sdcard/.m2/repository/'
|
url 'file:///sdcard/.m2/repository/'
|
||||||
@@ -11,19 +20,6 @@ buildscript {
|
|||||||
maven { url "https://nexus.winboll.cc/repository/maven-public/" }
|
maven { url "https://nexus.winboll.cc/repository/maven-public/" }
|
||||||
// "WinBoLL Snapshot"
|
// "WinBoLL Snapshot"
|
||||||
maven { url "https://nexus.winboll.cc/repository/maven-snapshots/" }
|
maven { url "https://nexus.winboll.cc/repository/maven-snapshots/" }
|
||||||
|
|
||||||
maven { url 'https://maven.aliyun.com/repository/public/' }
|
|
||||||
maven { url 'https://maven.aliyun.com/repository/google/' }
|
|
||||||
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
|
|
||||||
maven { url 'https://dl.bintray.com/ppartisan/maven/' }
|
|
||||||
maven { url "https://clojars.org/repo/" }
|
|
||||||
maven { url "https://jitpack.io" }
|
|
||||||
mavenCentral()
|
|
||||||
google()
|
|
||||||
//println "mavenLocal : ==========="
|
|
||||||
//println mavenLocal().url
|
|
||||||
//println "mavenLocal : ==========="
|
|
||||||
//mavenLocal()
|
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
// 适配MIUI12
|
// 适配MIUI12
|
||||||
@@ -35,6 +31,15 @@ buildscript {
|
|||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/public/' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/google/' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
|
||||||
|
maven { url 'https://dl.bintray.com/ppartisan/maven/' }
|
||||||
|
maven { url "https://clojars.org/repo/" }
|
||||||
|
maven { url "https://jitpack.io" }
|
||||||
|
mavenCentral()
|
||||||
|
google()
|
||||||
|
|
||||||
mavenLocal {
|
mavenLocal {
|
||||||
// 设置本地Maven仓库路径
|
// 设置本地Maven仓库路径
|
||||||
url 'file:///sdcard/.m2/repository/'
|
url 'file:///sdcard/.m2/repository/'
|
||||||
@@ -45,19 +50,6 @@ allprojects {
|
|||||||
maven { url "https://nexus.winboll.cc/repository/maven-public/" }
|
maven { url "https://nexus.winboll.cc/repository/maven-public/" }
|
||||||
// "WinBoLL Snapshot"
|
// "WinBoLL Snapshot"
|
||||||
maven { url "https://nexus.winboll.cc/repository/maven-snapshots/" }
|
maven { url "https://nexus.winboll.cc/repository/maven-snapshots/" }
|
||||||
|
|
||||||
maven { url 'https://maven.aliyun.com/repository/public/' }
|
|
||||||
maven { url 'https://maven.aliyun.com/repository/google/' }
|
|
||||||
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
|
|
||||||
maven { url 'https://dl.bintray.com/ppartisan/maven/' }
|
|
||||||
maven { url "https://clojars.org/repo/" }
|
|
||||||
maven { url "https://jitpack.io" }
|
|
||||||
mavenCentral()
|
|
||||||
google()
|
|
||||||
//println "mavenLocal : ==========="
|
|
||||||
//println mavenLocal().url
|
|
||||||
//println "mavenLocal : ==========="
|
|
||||||
//mavenLocal()
|
|
||||||
}
|
}
|
||||||
ext {
|
ext {
|
||||||
// 定义全局变量,常用于版本管理
|
// 定义全局变量,常用于版本管理
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
|
||||||
#Tue Mar 17 03:26:16 HKT 2026
|
|
||||||
stageCount=28
|
|
||||||
libraryProject=
|
|
||||||
baseVersion=15.0
|
|
||||||
publishVersion=15.0.27
|
|
||||||
buildCount=0
|
|
||||||
baseBetaVersion=15.0.28
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="app_name">DebugTemp☆</string>
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
|
||||||
<manifest
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="cc.winboll.studio.debugtemp">
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/MyAppTheme"
|
|
||||||
android:resizeableActivity="true"
|
|
||||||
android:name=".App">
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:label="@string/app_name">
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
|
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
|
||||||
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.max_aspect"
|
|
||||||
android:value="4.0"/>
|
|
||||||
|
|
||||||
<activity android:name=".GlobalApplication$CrashActivity"/>
|
|
||||||
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
package cc.winboll.studio.debugtemp;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.ClipData;
|
|
||||||
import android.content.ClipboardManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageInfo;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.Typeface;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.HorizontalScrollView;
|
|
||||||
import android.widget.ScrollView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
|
||||||
import cc.winboll.studio.libappbase.ToastUtils;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.lang.Thread.UncaughtExceptionHandler;
|
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
public class App extends GlobalApplication {
|
|
||||||
|
|
||||||
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
|
|
||||||
// 初始化 Toast 框架
|
|
||||||
ToastUtils.init(this);
|
|
||||||
|
|
||||||
//CrashHandler.getInstance().registerGlobal(this);
|
|
||||||
//CrashHandler.getInstance().registerPart(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void write(InputStream input, OutputStream output) throws IOException {
|
|
||||||
byte[] buf = new byte[1024 * 8];
|
|
||||||
int len;
|
|
||||||
while ((len = input.read(buf)) != -1) {
|
|
||||||
output.write(buf, 0, len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void write(File file, byte[] data) throws IOException {
|
|
||||||
File parent = file.getParentFile();
|
|
||||||
if (parent != null && !parent.exists()) parent.mkdirs();
|
|
||||||
|
|
||||||
ByteArrayInputStream input = new ByteArrayInputStream(data);
|
|
||||||
FileOutputStream output = new FileOutputStream(file);
|
|
||||||
try {
|
|
||||||
write(input, output);
|
|
||||||
} finally {
|
|
||||||
closeIO(input, output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String toString(InputStream input) throws IOException {
|
|
||||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
|
||||||
write(input, output);
|
|
||||||
try {
|
|
||||||
return output.toString("UTF-8");
|
|
||||||
} finally {
|
|
||||||
closeIO(input, output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void closeIO(Closeable... closeables) {
|
|
||||||
for (Closeable closeable : closeables) {
|
|
||||||
try {
|
|
||||||
if (closeable != null) closeable.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class CrashHandler {
|
|
||||||
|
|
||||||
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
|
|
||||||
|
|
||||||
private static CrashHandler sInstance;
|
|
||||||
|
|
||||||
private PartCrashHandler mPartCrashHandler;
|
|
||||||
|
|
||||||
public static CrashHandler getInstance() {
|
|
||||||
if (sInstance == null) {
|
|
||||||
sInstance = new CrashHandler();
|
|
||||||
}
|
|
||||||
return sInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void registerGlobal(Context context) {
|
|
||||||
registerGlobal(context, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void registerGlobal(Context context, String crashDir) {
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unregister() {
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void registerPart(Context context) {
|
|
||||||
unregisterPart(context);
|
|
||||||
mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
|
|
||||||
MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unregisterPart(Context context) {
|
|
||||||
if (mPartCrashHandler != null) {
|
|
||||||
mPartCrashHandler.isRunning.set(false);
|
|
||||||
mPartCrashHandler = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class PartCrashHandler implements Runnable {
|
|
||||||
|
|
||||||
private final Context mContext;
|
|
||||||
|
|
||||||
public AtomicBoolean isRunning = new AtomicBoolean(true);
|
|
||||||
|
|
||||||
public PartCrashHandler(Context context) {
|
|
||||||
this.mContext = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
while (isRunning.get()) {
|
|
||||||
try {
|
|
||||||
Looper.loop();
|
|
||||||
} catch (final Throwable e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
if (isRunning.get()) {
|
|
||||||
MAIN_HANDLER.post(new Runnable(){
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (e instanceof RuntimeException) {
|
|
||||||
throw (RuntimeException)e;
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
|
|
||||||
|
|
||||||
private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
|
|
||||||
|
|
||||||
private final Context mContext;
|
|
||||||
|
|
||||||
private final File mCrashDir;
|
|
||||||
|
|
||||||
public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
|
|
||||||
this.mContext = context;
|
|
||||||
this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void uncaughtException(Thread thread, Throwable throwable) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
String log = buildLog(throwable);
|
|
||||||
writeLog(log);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Intent intent = new Intent(mContext, CrashActivity.class);
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, log);
|
|
||||||
mContext.startActivity(intent);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
writeLog(e.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
throwable.printStackTrace();
|
|
||||||
android.os.Process.killProcess(android.os.Process.myPid());
|
|
||||||
System.exit(0);
|
|
||||||
|
|
||||||
} catch (Throwable e) {
|
|
||||||
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildLog(Throwable throwable) {
|
|
||||||
String time = DATE_FORMAT.format(new Date());
|
|
||||||
|
|
||||||
String versionName = "unknown";
|
|
||||||
long versionCode = 0;
|
|
||||||
try {
|
|
||||||
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
|
|
||||||
versionName = packageInfo.versionName;
|
|
||||||
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
|
|
||||||
} catch (Throwable ignored) {}
|
|
||||||
|
|
||||||
LinkedHashMap<String, String> head = new LinkedHashMap<String, String>();
|
|
||||||
head.put("Time Of Crash", time);
|
|
||||||
head.put("Device", String.format("%s, %s", Build.MANUFACTURER, Build.MODEL));
|
|
||||||
head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
|
|
||||||
head.put("App Version", String.format("%s (%d)", versionName, versionCode));
|
|
||||||
head.put("Kernel", getKernel());
|
|
||||||
head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS): "unknown");
|
|
||||||
head.put("Fingerprint", Build.FINGERPRINT);
|
|
||||||
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
|
|
||||||
for (String key : head.keySet()) {
|
|
||||||
if (builder.length() != 0) builder.append("\n");
|
|
||||||
builder.append(key);
|
|
||||||
builder.append(" : ");
|
|
||||||
builder.append(head.get(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.append("\n\n");
|
|
||||||
builder.append(Log.getStackTraceString(throwable));
|
|
||||||
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeLog(String log) {
|
|
||||||
String time = DATE_FORMAT.format(new Date());
|
|
||||||
File file = new File(mCrashDir, "crash_" + time + ".txt");
|
|
||||||
try {
|
|
||||||
write(file, log.getBytes("UTF-8"));
|
|
||||||
} catch (Throwable e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getKernel() {
|
|
||||||
try {
|
|
||||||
return App.toString(new FileInputStream("/proc/version")).trim();
|
|
||||||
} catch (Throwable e) {
|
|
||||||
return e.getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class CrashActivity extends Activity {
|
|
||||||
|
|
||||||
private String mLog;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
setTheme(android.R.style.Theme_DeviceDefault);
|
|
||||||
setTitle("App Crash");
|
|
||||||
|
|
||||||
mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
|
||||||
|
|
||||||
ScrollView contentView = new ScrollView(this);
|
|
||||||
contentView.setFillViewport(true);
|
|
||||||
|
|
||||||
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
|
|
||||||
|
|
||||||
TextView textView = new TextView(this);
|
|
||||||
int padding = dp2px(16);
|
|
||||||
textView.setPadding(padding, padding, padding, padding);
|
|
||||||
textView.setText(mLog);
|
|
||||||
textView.setTextIsSelectable(true);
|
|
||||||
textView.setTypeface(Typeface.DEFAULT);
|
|
||||||
textView.setLinksClickable(true);
|
|
||||||
|
|
||||||
horizontalScrollView.addView(textView);
|
|
||||||
contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
|
||||||
|
|
||||||
setContentView(contentView);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void restart() {
|
|
||||||
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
|
|
||||||
if (intent != null) {
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
finish();
|
|
||||||
android.os.Process.killProcess(android.os.Process.myPid());
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int dp2px(float dpValue) {
|
|
||||||
final float scale = Resources.getSystem().getDisplayMetrics().density;
|
|
||||||
return (int) (dpValue * scale + 0.5f);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
menu.add(0, android.R.id.copy, 0, android.R.string.copy)
|
|
||||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
|
||||||
return super.onCreateOptionsMenu(menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case android.R.id.copy:
|
|
||||||
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
|
||||||
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package cc.winboll.studio.debugtemp;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import cc.winboll.studio.libappbase.LogView;
|
|
||||||
import cc.winboll.studio.libappbase.ToastUtils;
|
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
|
||||||
|
|
||||||
LogView mLogView;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_main);
|
|
||||||
|
|
||||||
Toolbar toolbar=(Toolbar)findViewById(R.id.toolbar);
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
|
|
||||||
mLogView = findViewById(R.id.logview);
|
|
||||||
|
|
||||||
ToastUtils.show("onCreate");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
mLogView.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?attr/actionBarSize"
|
|
||||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1.0"
|
|
||||||
android:gravity="center_vertical|center_horizontal">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="DebugTemp"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceLarge"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1.0">
|
|
||||||
|
|
||||||
<cc.winboll.studio.libappbase.LogView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:id="@+id/logview"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<resources>
|
|
||||||
<string name="app_name">DebugTemp</string>
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# WinBoLL
|
# Gallery
|
||||||
|
|
||||||
#### 介绍
|
#### 介绍
|
||||||
WinBoLL 应用编译功能临时调试项目
|
云宝相册应用
|
||||||
|
|
||||||
#### 软件架构
|
#### 软件架构
|
||||||
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
||||||
@@ -10,12 +10,9 @@ WinBoLL 应用编译功能临时调试项目
|
|||||||
|
|
||||||
#### Gradle 编译说明
|
#### Gradle 编译说明
|
||||||
调试版编译命令 :gradle assembleBetaDebug
|
调试版编译命令 :gradle assembleBetaDebug
|
||||||
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh debugtemp
|
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh gallery
|
||||||
|
|
||||||
#### 使用说明
|
#### 使用说明
|
||||||
3. Termux应用配置:
|
|
||||||
- 已安装Termux(包名 com.termux );
|
|
||||||
- 执行 echo "allow-external-apps = true" > ~/.termux/termux.properties
|
|
||||||
|
|
||||||
#### 参与贡献
|
#### 参与贡献
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ android {
|
|||||||
buildToolsVersion "30.0.3"
|
buildToolsVersion "30.0.3"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "cc.winboll.studio.debugtemp"
|
applicationId "cc.winboll.studio.gallery"
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
// 适配MIUI12
|
// 适配MIUI12
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
@@ -103,13 +103,13 @@ dependencies {
|
|||||||
implementation "io.noties.markwon:linkify:$markwonVersion"
|
implementation "io.noties.markwon:linkify:$markwonVersion"
|
||||||
implementation "io.noties.markwon:recycler:$markwonVersion"
|
implementation "io.noties.markwon:recycler:$markwonVersion"
|
||||||
*/
|
*/
|
||||||
implementation 'com.termux:terminal-emulator:0.118.0'
|
/*implementation 'com.termux:terminal-emulator:0.118.0'
|
||||||
implementation 'com.termux:terminal-view:0.118.0'
|
implementation 'com.termux:terminal-view:0.118.0'
|
||||||
implementation 'com.termux:termux-shared:0.118.0'
|
implementation 'com.termux:termux-shared:0.118.0'
|
||||||
|
*/
|
||||||
// WinBoLL库 nexus.winboll.cc 地址
|
// WinBoLL库 nexus.winboll.cc 地址
|
||||||
api 'cc.winboll.studio:libaes:15.15.2'
|
api 'cc.winboll.studio:libaes:15.15.9'
|
||||||
api 'cc.winboll.studio:libappbase:15.15.11'
|
api 'cc.winboll.studio:libappbase:15.15.21'
|
||||||
|
|
||||||
// WinBoLL备用库 jitpack.io 地址
|
// WinBoLL备用库 jitpack.io 地址
|
||||||
//api 'com.github.ZhanGSKen:AES:aes-v15.15.7'
|
//api 'com.github.ZhanGSKen:AES:aes-v15.15.7'
|
||||||
8
gallery/build.properties
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
|
#Sat May 02 10:32:03 CST 2026
|
||||||
|
stageCount=16
|
||||||
|
libraryProject=
|
||||||
|
baseVersion=15.0
|
||||||
|
publishVersion=15.0.15
|
||||||
|
buildCount=19
|
||||||
|
baseBetaVersion=15.0.16
|
||||||
6
gallery/src/beta/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Put flavor specific strings here -->
|
||||||
|
<string name="app_name">Gallery+</string>
|
||||||
|
</resources>
|
||||||
66
gallery/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="cc.winboll.studio.gallery">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/AppTheme"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
android:name=".GlobalWinBoLLApplication"
|
||||||
|
android:requestLegacyExternalStorage="true">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/app_name">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".SettingsActivity"
|
||||||
|
android:label="@string/settings_title"/>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".AlbumActivity"
|
||||||
|
android:label="@string/app_name"/>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ImageViewerActivity"
|
||||||
|
android:label="@string/app_name"/>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".TrashActivity"
|
||||||
|
android:label="@string/trash"/>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".CropActivity"
|
||||||
|
android:label="调整封面"/>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".AboutActivity"
|
||||||
|
android:label="@string/about"/>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.max_aspect"
|
||||||
|
android:value="4.0"/>
|
||||||
|
|
||||||
|
<activity android:name=".GlobalApplication$CrashActivity"/>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.libappbase.models.APPInfo;
|
||||||
|
import cc.winboll.studio.libappbase.views.AboutView;
|
||||||
|
|
||||||
|
public class AboutActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
public static final String TAG = "AboutActivity";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_about);
|
||||||
|
|
||||||
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AboutView aboutView = findViewById(R.id.aboutview);
|
||||||
|
aboutView.setAPPInfo(genDefaultAppInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
private APPInfo genDefaultAppInfo() {
|
||||||
|
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
|
||||||
|
String branchName = "gallery";
|
||||||
|
APPInfo appInfo = new APPInfo();
|
||||||
|
appInfo.setAppName("Gallery");
|
||||||
|
appInfo.setAppIcon(R.drawable.ic_winboll);
|
||||||
|
appInfo.setAppDescription(getString(R.string.app_description));
|
||||||
|
appInfo.setAppGitName("WinBoLL");
|
||||||
|
appInfo.setAppGitOwner("Studio");
|
||||||
|
appInfo.setAppGitAPPBranch(branchName);
|
||||||
|
appInfo.setAppGitAPPSubProjectFolder(branchName);
|
||||||
|
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=Gallery");
|
||||||
|
appInfo.setAppAPKName("Gallery");
|
||||||
|
appInfo.setAppAPKFolderName("Gallery");
|
||||||
|
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
|
||||||
|
return appInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
gallery/src/main/java/cc/winboll/studio/gallery/Album.java
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
public class Album {
|
||||||
|
public static final String TAG = "Album";
|
||||||
|
private String name;
|
||||||
|
private String path;
|
||||||
|
private Uri coverUri;
|
||||||
|
private int imageCount;
|
||||||
|
|
||||||
|
public Album(String name, String path, Uri coverUri, int imageCount) {
|
||||||
|
LogUtils.d(TAG, "Album created: " + name);
|
||||||
|
this.name = name;
|
||||||
|
this.path = path;
|
||||||
|
this.coverUri = coverUri;
|
||||||
|
this.imageCount = imageCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() { return name; }
|
||||||
|
public String getPath() { return path; }
|
||||||
|
public Uri getCoverUri() { return coverUri; }
|
||||||
|
public int getImageCount() { return imageCount; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.core.app.ActivityCompat;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.gallery.ImageAdapter.OnImageClickListener;
|
||||||
|
|
||||||
|
public class AlbumActivity extends AppCompatActivity {
|
||||||
|
public static final String TAG = "AlbumActivity";
|
||||||
|
private static final int PERMISSION_REQUEST_CODE = 101;
|
||||||
|
public static final String EXTRA_ALBUM_PATH = "album_path";
|
||||||
|
public static final String EXTRA_ALBUM_NAME = "album_name";
|
||||||
|
private RecyclerView recyclerView;
|
||||||
|
private ImageAdapter adapter;
|
||||||
|
private String albumPath;
|
||||||
|
private String albumName;
|
||||||
|
private FloatingActionButton fabScrollTop;
|
||||||
|
private Preferences prefs;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_main);
|
||||||
|
LogUtils.d(TAG, "onCreate");
|
||||||
|
|
||||||
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
|
||||||
|
albumPath = getIntent().getStringExtra(EXTRA_ALBUM_PATH);
|
||||||
|
albumName = getIntent().getStringExtra(EXTRA_ALBUM_NAME);
|
||||||
|
|
||||||
|
getSupportActionBar().setTitle(albumName);
|
||||||
|
|
||||||
|
prefs = new Preferences(this);
|
||||||
|
|
||||||
|
recyclerView = findViewById(R.id.recycler_view);
|
||||||
|
recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
|
||||||
|
adapter = new ImageAdapter();
|
||||||
|
adapter.setContext(this);
|
||||||
|
adapter.setAlbumPath(albumPath);
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
|
fabScrollTop = findViewById(R.id.fab_scroll_top);
|
||||||
|
fabScrollTop.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
recyclerView.scrollToPosition(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy);
|
||||||
|
GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
|
||||||
|
if (layoutManager != null) {
|
||||||
|
int firstVisible = layoutManager.findFirstVisibleItemPosition();
|
||||||
|
if (firstVisible > 0) {
|
||||||
|
fabScrollTop.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
fabScrollTop.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
adapter.setOnImageClickListener(new OnImageClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onImageClick(int position, ArrayList<Uri> urls, ArrayList<String> paths) {
|
||||||
|
Intent intent = new Intent(AlbumActivity.this, ImageViewerActivity.class);
|
||||||
|
intent.putParcelableArrayListExtra(ImageViewerActivity.EXTRA_IMAGE_URLS, urls);
|
||||||
|
intent.putStringArrayListExtra(ImageViewerActivity.EXTRA_POSITIONS, paths);
|
||||||
|
intent.putExtra(ImageViewerActivity.EXTRA_POSITION, position);
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (checkPermission()) {
|
||||||
|
loadImages();
|
||||||
|
} else {
|
||||||
|
requestPermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkPermission() {
|
||||||
|
return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
== PackageManager.PERMISSION_GRANTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestPermission() {
|
||||||
|
ActivityCompat.requestPermissions(this,
|
||||||
|
new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE},
|
||||||
|
PERMISSION_REQUEST_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||||
|
@NonNull int[] grantResults) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
if (requestCode == PERMISSION_REQUEST_CODE) {
|
||||||
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
loadImages();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getSortOrder(int sortMode) {
|
||||||
|
switch (sortMode) {
|
||||||
|
case Preferences.SORT_TIME_ASC:
|
||||||
|
return android.provider.MediaStore.Images.Media.DATE_ADDED + " ASC";
|
||||||
|
case Preferences.SORT_NAME_DESC:
|
||||||
|
return android.provider.MediaStore.Images.Media.DISPLAY_NAME + " DESC";
|
||||||
|
case Preferences.SORT_NAME_ASC:
|
||||||
|
return android.provider.MediaStore.Images.Media.DISPLAY_NAME + " ASC";
|
||||||
|
case Preferences.SORT_TIME_DESC:
|
||||||
|
default:
|
||||||
|
return android.provider.MediaStore.Images.Media.DATE_ADDED + " DESC";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadImages() {
|
||||||
|
LogUtils.d(TAG, "loadImages");
|
||||||
|
ArrayList<Uri> imageUrls = new ArrayList<>();
|
||||||
|
ArrayList<String> imagePaths = new ArrayList<>();
|
||||||
|
ContentResolver contentResolver = getContentResolver();
|
||||||
|
Uri collection = android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||||
|
|
||||||
|
String selection = android.provider.MediaStore.Images.Media.DATA + " LIKE ?";
|
||||||
|
String[] selectionArgs = new String[]{albumPath + "%"};
|
||||||
|
int sortMode = prefs.getAlbumSortMode();
|
||||||
|
String sortOrder = getSortOrder(sortMode);
|
||||||
|
|
||||||
|
try (Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder)) {
|
||||||
|
if (cursor != null) {
|
||||||
|
int dataColumn = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.DATA);
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
String path = cursor.getString(dataColumn);
|
||||||
|
if (path != null && path.startsWith(albumPath + "/")) {
|
||||||
|
long id = cursor.getLong(cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media._ID));
|
||||||
|
Uri contentUri = Uri.withAppendedPath(collection, String.valueOf(id));
|
||||||
|
imageUrls.add(contentUri);
|
||||||
|
imagePaths.add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageUrls.isEmpty()) {
|
||||||
|
Toast.makeText(this, R.string.no_images_found, Toast.LENGTH_SHORT).show();
|
||||||
|
LogUtils.i(TAG, "No images found");
|
||||||
|
}
|
||||||
|
adapter.setData(imageUrls, imagePaths);
|
||||||
|
LogUtils.d(TAG, "Loaded " + imageUrls.size() + " images");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.menu_album, menu);
|
||||||
|
int sortMode = prefs.getAlbumSortMode();
|
||||||
|
int menuId = getSortMenuId(sortMode);
|
||||||
|
MenuItem item = menu.findItem(menuId);
|
||||||
|
if (item != null) {
|
||||||
|
item.setChecked(true);
|
||||||
|
}
|
||||||
|
MenuItem sortItem = menu.findItem(R.id.action_sort);
|
||||||
|
if (sortItem != null && sortItem.getSubMenu() != null) {
|
||||||
|
sortItem.getSubMenu().setGroupCheckable(0, true, true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getSortMenuId(int sortMode) {
|
||||||
|
switch (sortMode) {
|
||||||
|
case Preferences.SORT_TIME_ASC:
|
||||||
|
return R.id.sort_time_asc;
|
||||||
|
case Preferences.SORT_NAME_DESC:
|
||||||
|
return R.id.sort_name_desc;
|
||||||
|
case Preferences.SORT_NAME_ASC:
|
||||||
|
return R.id.sort_name_asc;
|
||||||
|
case Preferences.SORT_TIME_DESC:
|
||||||
|
default:
|
||||||
|
return R.id.sort_time_desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||||
|
int itemId = item.getItemId();
|
||||||
|
if (itemId == R.id.sort_time_desc) {
|
||||||
|
prefs.setAlbumSortMode(Preferences.SORT_TIME_DESC);
|
||||||
|
item.setChecked(true);
|
||||||
|
loadImages();
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.sort_time_asc) {
|
||||||
|
prefs.setAlbumSortMode(Preferences.SORT_TIME_ASC);
|
||||||
|
item.setChecked(true);
|
||||||
|
loadImages();
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.sort_name_desc) {
|
||||||
|
prefs.setAlbumSortMode(Preferences.SORT_NAME_DESC);
|
||||||
|
item.setChecked(true);
|
||||||
|
loadImages();
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.sort_name_asc) {
|
||||||
|
prefs.setAlbumSortMode(Preferences.SORT_NAME_ASC);
|
||||||
|
item.setChecked(true);
|
||||||
|
loadImages();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (checkPermission()) {
|
||||||
|
loadImages();
|
||||||
|
}
|
||||||
|
if (adapter != null) {
|
||||||
|
adapter.refreshBg();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
if (requestCode == 100 && resultCode == RESULT_OK) {
|
||||||
|
loadImages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
import android.view.View.OnLongClickListener;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
public class AlbumAdapter extends RecyclerView.Adapter<AlbumAdapter.ViewHolder> {
|
||||||
|
public static final String TAG = "AlbumAdapter";
|
||||||
|
private ArrayList<Album> albums = new ArrayList<>();
|
||||||
|
private OnAlbumClickListener listener;
|
||||||
|
private Preferences prefs;
|
||||||
|
private int bgType = 0;
|
||||||
|
private PinnedAlbumDbHelper pinnedDbHelper;
|
||||||
|
private OnCoverSizeListener coverSizeListener;
|
||||||
|
private boolean coverSizeReported = false;
|
||||||
|
|
||||||
|
private int getBgRes() {
|
||||||
|
switch (bgType) {
|
||||||
|
case 0:
|
||||||
|
return R.drawable.bg_checkerboard;
|
||||||
|
case 1:
|
||||||
|
return R.drawable.bg_white;
|
||||||
|
case 2:
|
||||||
|
return R.drawable.bg_black;
|
||||||
|
default:
|
||||||
|
return R.drawable.bg_checkerboard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnAlbumClickListener {
|
||||||
|
void onAlbumClick(Album album);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnCoverSizeListener {
|
||||||
|
void onCoverSize(int width, int height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final int DEFAULT_COVER_WIDTH = 240;
|
||||||
|
public static final int DEFAULT_COVER_HEIGHT = 120;
|
||||||
|
|
||||||
|
public void setOnAlbumClickListener(OnAlbumClickListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnCoverSizeListener(OnCoverSizeListener listener) {
|
||||||
|
this.coverSizeListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setData(ArrayList<Album> albums) {
|
||||||
|
this.albums = sortAlbums(albums);
|
||||||
|
LogUtils.d(TAG, "setData: " + albums.size() + " albums");
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayList<Album> sortAlbums(ArrayList<Album> list) {
|
||||||
|
if (pinnedDbHelper == null || list == null || list.isEmpty()) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
ArrayList<Album> pinned = new ArrayList<>();
|
||||||
|
ArrayList<Album> unpinned = new ArrayList<>();
|
||||||
|
for (Album album : list) {
|
||||||
|
if (pinnedDbHelper.isPinned(album.getPath())) {
|
||||||
|
pinned.add(album);
|
||||||
|
} else {
|
||||||
|
unpinned.add(album);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pinned.addAll(unpinned);
|
||||||
|
return pinned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContext(android.content.Context context) {
|
||||||
|
prefs = new Preferences(context);
|
||||||
|
bgType = prefs.getBgType();
|
||||||
|
pinnedDbHelper = PinnedAlbumDbHelper.getInstance(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshBg() {
|
||||||
|
if (prefs != null) {
|
||||||
|
bgType = prefs.getBgType();
|
||||||
|
}
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshCover() {
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshPinned() {
|
||||||
|
if (pinnedDbHelper != null && albums != null && !albums.isEmpty()) {
|
||||||
|
ArrayList<Album> pinned = new ArrayList<>();
|
||||||
|
ArrayList<Album> unpinned = new ArrayList<>();
|
||||||
|
for (Album album : albums) {
|
||||||
|
if (pinnedDbHelper.isPinned(album.getPath())) {
|
||||||
|
pinned.add(album);
|
||||||
|
} else {
|
||||||
|
unpinned.add(album);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Comparator<Album> nameComparator = new Comparator<Album>() {
|
||||||
|
@Override
|
||||||
|
public int compare(Album a1, Album a2) {
|
||||||
|
return a1.getName().compareToIgnoreCase(a2.getName());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Collections.sort(pinned, nameComparator);
|
||||||
|
Collections.sort(unpinned, nameComparator);
|
||||||
|
albums.clear();
|
||||||
|
albums.addAll(pinned);
|
||||||
|
albums.addAll(unpinned);
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showContextMenu(View view, final Album album) {
|
||||||
|
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext());
|
||||||
|
builder.setTitle(album.getName());
|
||||||
|
final boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(album.getPath());
|
||||||
|
String[] items = isPinned ? new String[]{"取消置顶"} : new String[]{"置顶"};
|
||||||
|
builder.setItems(items, new android.content.DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(android.content.DialogInterface dialog, int which) {
|
||||||
|
if (pinnedDbHelper != null) {
|
||||||
|
if (which == 0) {
|
||||||
|
if (isPinned) {
|
||||||
|
pinnedDbHelper.unpinAlbum(album.getPath());
|
||||||
|
} else {
|
||||||
|
pinnedDbHelper.pinAlbum(album.getPath());
|
||||||
|
}
|
||||||
|
refreshPinned();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.item_album, parent, false);
|
||||||
|
return new ViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
|
||||||
|
final Album album = albums.get(position);
|
||||||
|
LogUtils.d(TAG, "bind: " + album.getName() + ", cover=" + album.getCoverUri());
|
||||||
|
|
||||||
|
holder.coverImage.setBackgroundResource(getBgRes());
|
||||||
|
|
||||||
|
boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(album.getPath());
|
||||||
|
holder.pinIcon.setVisibility(isPinned ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onLongClick(View v) {
|
||||||
|
showContextMenu(holder.itemView, album);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
holder.albumName.setText(album.getName());
|
||||||
|
holder.imageCount.setText(album.getImageCount() + " photos");
|
||||||
|
|
||||||
|
holder.itemView.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (!coverSizeReported && prefs != null) {
|
||||||
|
int savedWidth = prefs.getCoverWidth();
|
||||||
|
if (savedWidth == AlbumAdapter.DEFAULT_COVER_WIDTH) {
|
||||||
|
int width = holder.coverImage.getWidth();
|
||||||
|
int height = holder.coverImage.getHeight();
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
prefs.setCoverWidth(width);
|
||||||
|
prefs.setCoverHeight(height);
|
||||||
|
float ratio = (float) width / height;
|
||||||
|
prefs.setCoverRatio(ratio);
|
||||||
|
coverSizeReported = true;
|
||||||
|
if (coverSizeListener != null) {
|
||||||
|
coverSizeListener.onCoverSize(width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
holder.itemView.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onAlbumClick(album);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Uri coverUri = album.getCoverUri();
|
||||||
|
if (coverUri != null) {
|
||||||
|
String uriString = coverUri.toString();
|
||||||
|
LogUtils.d(TAG, "uri scheme: " + coverUri.getScheme() + ", path: " + uriString);
|
||||||
|
|
||||||
|
if ("file".equals(coverUri.getScheme())) {
|
||||||
|
File coverFile = new File(coverUri.getPath());
|
||||||
|
if (coverFile.exists()) {
|
||||||
|
Glide.with(holder.coverImage.getContext())
|
||||||
|
.load(coverFile)
|
||||||
|
.fitCenter()
|
||||||
|
.into(holder.coverImage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Glide.with(holder.coverImage.getContext())
|
||||||
|
.load(coverUri)
|
||||||
|
.fitCenter()
|
||||||
|
.into(holder.coverImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return albums.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
ImageView coverImage;
|
||||||
|
ImageView pinIcon;
|
||||||
|
TextView albumName;
|
||||||
|
TextView imageCount;
|
||||||
|
ViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
coverImage = itemView.findViewById(R.id.album_cover);
|
||||||
|
pinIcon = itemView.findViewById(R.id.pin_icon);
|
||||||
|
albumName = itemView.findViewById(R.id.album_name);
|
||||||
|
imageCount = itemView.findViewById(R.id.image_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
public class AlbumCoverDbHelper extends SQLiteOpenHelper {
|
||||||
|
public static final String TAG = "AlbumCoverDbHelper";
|
||||||
|
private static final String DB_NAME = "album_cover.db";
|
||||||
|
private static final int DB_VERSION = 2;
|
||||||
|
private static final String TABLE_NAME = "album_covers";
|
||||||
|
private static final String COLUMN_ALBUM_PATH = "album_path";
|
||||||
|
private static final String COLUMN_IMAGE_PATH = "image_path";
|
||||||
|
private static final String COLUMN_CROP_PATH = "crop_path";
|
||||||
|
|
||||||
|
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
|
||||||
|
+ COLUMN_ALBUM_PATH + " TEXT PRIMARY KEY, "
|
||||||
|
+ COLUMN_IMAGE_PATH + " TEXT, "
|
||||||
|
+ COLUMN_CROP_PATH + " TEXT)";
|
||||||
|
|
||||||
|
private static AlbumCoverDbHelper dbHelper;
|
||||||
|
|
||||||
|
public static AlbumCoverDbHelper getInstance(Context context) {
|
||||||
|
if (dbHelper == null) {
|
||||||
|
dbHelper = new AlbumCoverDbHelper(context.getApplicationContext());
|
||||||
|
}
|
||||||
|
return dbHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlbumCoverDbHelper(Context context) {
|
||||||
|
super(context, DB_NAME, null, DB_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(SQLiteDatabase db) {
|
||||||
|
db.execSQL(SQL_CREATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||||
|
if (oldVersion < 2) {
|
||||||
|
try {
|
||||||
|
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COLUMN_CROP_PATH + " TEXT");
|
||||||
|
} catch (Exception e) {
|
||||||
|
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||||
|
onCreate(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCoverWithCrop(String albumPath, String imagePath, String cropPath) {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(COLUMN_ALBUM_PATH, albumPath);
|
||||||
|
values.put(COLUMN_IMAGE_PATH, imagePath);
|
||||||
|
values.put(COLUMN_CROP_PATH, cropPath);
|
||||||
|
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
|
LogUtils.d(TAG, "setCoverWithCrop: album=" + albumPath + ", image=" + imagePath + ", crop=" + cropPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCover(String albumPath, String imagePath) {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(COLUMN_ALBUM_PATH, albumPath);
|
||||||
|
values.put(COLUMN_IMAGE_PATH, imagePath);
|
||||||
|
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
|
LogUtils.d(TAG, "setCover: album=" + albumPath + ", image=" + imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCover(String albumPath) {
|
||||||
|
SQLiteDatabase db = getReadableDatabase();
|
||||||
|
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_CROP_PATH, COLUMN_IMAGE_PATH},
|
||||||
|
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
|
||||||
|
String coverPath = null;
|
||||||
|
if (cursor != null) {
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
coverPath = cursor.getString(0);
|
||||||
|
if (coverPath == null) {
|
||||||
|
coverPath = cursor.getString(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
return coverPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCropPath(String albumPath) {
|
||||||
|
SQLiteDatabase db = getReadableDatabase();
|
||||||
|
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_CROP_PATH},
|
||||||
|
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
|
||||||
|
String cropPath = null;
|
||||||
|
if (cursor != null) {
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
cropPath = cursor.getString(0);
|
||||||
|
}
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
return cropPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOriginalImagePath(String albumPath) {
|
||||||
|
SQLiteDatabase db = getReadableDatabase();
|
||||||
|
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_IMAGE_PATH},
|
||||||
|
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
|
||||||
|
String imagePath = null;
|
||||||
|
if (cursor != null) {
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
imagePath = cursor.getString(0);
|
||||||
|
}
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
return imagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearCover(String albumPath) {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.putNull(COLUMN_CROP_PATH);
|
||||||
|
db.update(TABLE_NAME, values, COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath});
|
||||||
|
LogUtils.d(TAG, "clearCover: " + albumPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteCover(String albumPath) {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
db.delete(TABLE_NAME, COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath});
|
||||||
|
LogUtils.d(TAG, "deleteCover: " + albumPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.SeekBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class CropActivity extends AppCompatActivity {
|
||||||
|
public static final String TAG = "CropActivity";
|
||||||
|
public static final String EXTRA_IMAGE_URI = "image_uri";
|
||||||
|
public static final String EXTRA_IMAGE_PATH = "image_path";
|
||||||
|
public static final String EXTRA_ALBUM_PATH = "album_path";
|
||||||
|
public static final String EXTRA_CROP_WIDTH = "crop_width";
|
||||||
|
public static final String EXTRA_CROP_HEIGHT = "crop_height";
|
||||||
|
|
||||||
|
private CropCanvasView cropCanvasView;
|
||||||
|
private ZoomContainerView zoomContainer;
|
||||||
|
private Bitmap originalBitmap;
|
||||||
|
private String imagePath;
|
||||||
|
private String albumPath;
|
||||||
|
private int cropWidth = 240;
|
||||||
|
private int cropHeight = 120;
|
||||||
|
private float cropRatio = 2.0f;
|
||||||
|
private Preferences prefs;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_crop);
|
||||||
|
|
||||||
|
imagePath = getIntent().getStringExtra(EXTRA_IMAGE_PATH);
|
||||||
|
albumPath = getIntent().getStringExtra(EXTRA_ALBUM_PATH);
|
||||||
|
cropWidth = getIntent().getIntExtra(EXTRA_CROP_WIDTH, 240);
|
||||||
|
cropHeight = getIntent().getIntExtra(EXTRA_CROP_HEIGHT, 120);
|
||||||
|
|
||||||
|
prefs = new Preferences(this);
|
||||||
|
int bgType = prefs.getBgType();
|
||||||
|
if (cropWidth > 0 && cropHeight > 0) {
|
||||||
|
cropRatio = (float) cropWidth / cropHeight;
|
||||||
|
} else {
|
||||||
|
cropRatio = prefs.getCoverRatio();
|
||||||
|
cropWidth = (int) (120 * cropRatio);
|
||||||
|
cropHeight = 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById(R.id.btn_close).setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
findViewById(R.id.btn_done).setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
saveCroppedCover();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
findViewById(R.id.btn_info).setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
showCropInfoDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
findViewById(R.id.btn_change_bg).setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
Toast.makeText(CropActivity.this, "修改剪裁背景颜色", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
zoomContainer = findViewById(R.id.zoom_container);
|
||||||
|
|
||||||
|
SeekBar seekBarZoom = findViewById(R.id.seekbar_zoom);
|
||||||
|
seekBarZoom.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||||
|
if (zoomContainer != null && fromUser) {
|
||||||
|
float scale = 0.1f + (progress / 100f) * 4.9f;
|
||||||
|
zoomContainer.setScaleFactor(scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
cropCanvasView = findViewById(R.id.crop_canvas_view);
|
||||||
|
|
||||||
|
cropCanvasView.setBackgroundType(bgType);
|
||||||
|
|
||||||
|
cropCanvasView.setOnBackgroundColorChangedListener(new CropCanvasView.OnBackgroundColorChangedListener() {
|
||||||
|
@Override
|
||||||
|
public void onBackgroundColorChanged(int color) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loadImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadImage() {
|
||||||
|
try {
|
||||||
|
if (imagePath != null && new File(imagePath).exists()) {
|
||||||
|
originalBitmap = BitmapFactory.decodeFile(imagePath);
|
||||||
|
} else {
|
||||||
|
Uri imageUri = getIntent().getParcelableExtra(EXTRA_IMAGE_URI);
|
||||||
|
if (imageUri != null) {
|
||||||
|
originalBitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), imageUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalBitmap != null) {
|
||||||
|
cropCanvasView.setImageBitmap(originalBitmap);
|
||||||
|
cropCanvasView.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
initCrop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "loadImage error: " + e.getMessage());
|
||||||
|
Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initCrop() {
|
||||||
|
cropCanvasView.initCanvas(originalBitmap.getWidth(), originalBitmap.getHeight(), cropRatio);
|
||||||
|
|
||||||
|
int viewW = cropCanvasView.getWidth();
|
||||||
|
int viewH = cropCanvasView.getHeight();
|
||||||
|
if (viewW > 0 && viewH > 0) {
|
||||||
|
cropCanvasView.scaleToView(viewW, viewH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveCroppedCover() {
|
||||||
|
if (originalBitmap == null || originalBitmap.isRecycled()) {
|
||||||
|
Toast.makeText(this, "Failed to get image", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Bitmap canvasBitmap = cropCanvasView.getCanvasBitmap();
|
||||||
|
if (canvasBitmap == null || canvasBitmap.isRecycled()) {
|
||||||
|
Toast.makeText(this, "Failed to get canvas bitmap", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RectF cropRect = cropCanvasView.getCropRect();
|
||||||
|
|
||||||
|
int bmpX = (int) cropRect.left;
|
||||||
|
int bmpY = (int) cropRect.top;
|
||||||
|
int bmpW = (int) cropRect.width();
|
||||||
|
int bmpH = (int) cropRect.height();
|
||||||
|
|
||||||
|
bmpX = Math.max(0, Math.min(bmpX, canvasBitmap.getWidth() - 1));
|
||||||
|
bmpY = Math.max(0, Math.min(bmpY, canvasBitmap.getHeight() - 1));
|
||||||
|
bmpW = Math.max(1, Math.min(bmpW, canvasBitmap.getWidth() - bmpX));
|
||||||
|
bmpH = Math.max(1, Math.min(bmpH, canvasBitmap.getHeight() - bmpY));
|
||||||
|
|
||||||
|
LogUtils.d(TAG, "saveCroppedCover: cropRect=" + cropRect);
|
||||||
|
LogUtils.d(TAG, "saveCroppedCover: crop=(" + bmpX + "," + bmpY + "," + bmpW + "," + bmpH + ")");
|
||||||
|
LogUtils.d(TAG, "saveCroppedCover: canvas size=" + canvasBitmap.getWidth() + "x" + canvasBitmap.getHeight());
|
||||||
|
|
||||||
|
Bitmap cropped = Bitmap.createBitmap(canvasBitmap, bmpX, bmpY, bmpW, bmpH);
|
||||||
|
canvasBitmap.recycle();
|
||||||
|
|
||||||
|
LogUtils.d(TAG, "saveCroppedCover: cropped size=" + cropped.getWidth() + "x" + cropped.getHeight());
|
||||||
|
|
||||||
|
File coverDir = new File(getFilesDir(), "covers");
|
||||||
|
if (!coverDir.exists()) {
|
||||||
|
coverDir.mkdirs();
|
||||||
|
}
|
||||||
|
File coverFile = new File(coverDir, "cover_" + System.currentTimeMillis() + ".png");
|
||||||
|
LogUtils.d(TAG, "saveCroppedCover: cover file=" + coverFile.getAbsolutePath());
|
||||||
|
|
||||||
|
FileOutputStream fos = new FileOutputStream(coverFile);
|
||||||
|
cropped.compress(Bitmap.CompressFormat.PNG, 100, fos);
|
||||||
|
fos.close();
|
||||||
|
|
||||||
|
LogUtils.d(TAG, "saveCroppedCover: file exists=" + coverFile.exists() + ", length=" + coverFile.length());
|
||||||
|
|
||||||
|
cropped.recycle();
|
||||||
|
|
||||||
|
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
|
||||||
|
coverDbHelper.setCoverWithCrop(albumPath, imagePath, coverFile.getAbsolutePath());
|
||||||
|
LogUtils.d(TAG, "saveCroppedCover: cover saved to db, albumPath=" + albumPath);
|
||||||
|
|
||||||
|
Intent broadcastIntent = new Intent(Preferences.ACTION_COVER_UPDATED);
|
||||||
|
sendBroadcast(broadcastIntent);
|
||||||
|
|
||||||
|
Toast.makeText(this, "封面已保存", Toast.LENGTH_SHORT).show();
|
||||||
|
setResult(RESULT_OK);
|
||||||
|
finish();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "saveCroppedCover error: " + e.getMessage());
|
||||||
|
Toast.makeText(this, "Failed to save cover", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showCropInfoDialog() {
|
||||||
|
StringBuilder info = new StringBuilder();
|
||||||
|
info.append("=== 画布信息 ===\n");
|
||||||
|
info.append("画布宽度: ").append(cropCanvasView.getCanvasWidth()).append("px\n");
|
||||||
|
info.append("画布高度: ").append(cropCanvasView.getCanvasHeight()).append("px\n");
|
||||||
|
|
||||||
|
info.append("\n=== 背景类型 ===\n");
|
||||||
|
String[] bgNames = {"灰白相间", "全白", "全黑"};
|
||||||
|
info.append("背景: ").append(bgNames[cropCanvasView.getBackgroundType()]).append("\n");
|
||||||
|
int bgColor = cropCanvasView.getBackgroundColor();
|
||||||
|
info.append("背景颜色: #").append(String.format("%06X", bgColor & 0xFFFFFF)).append("\n");
|
||||||
|
info.append("拾取坐标: ").append(cropCanvasView.getLastPickImageX()).append(",")
|
||||||
|
.append(cropCanvasView.getLastPickImageY()).append("\n");
|
||||||
|
|
||||||
|
info.append("\n=== 裁剪结果 ===\n");
|
||||||
|
RectF cropRect = cropCanvasView.getCropRect();
|
||||||
|
if (cropRect != null) {
|
||||||
|
info.append("裁剪区域: ").append((int)cropRect.left).append(",")
|
||||||
|
.append((int)cropRect.top).append(",")
|
||||||
|
.append((int)cropRect.right).append(",")
|
||||||
|
.append((int)cropRect.bottom).append("\n");
|
||||||
|
info.append("裁剪宽度: ").append((int)cropRect.width()).append("px\n");
|
||||||
|
info.append("裁剪高度: ").append((int)cropRect.height()).append("px\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitmap canvasBitmap = cropCanvasView.getCanvasBitmap();
|
||||||
|
final Bitmap[] previewHolder = new Bitmap[1];
|
||||||
|
if (canvasBitmap != null && !canvasBitmap.isRecycled() && cropRect != null) {
|
||||||
|
int bmpX = Math.max(0, (int) cropRect.left);
|
||||||
|
int bmpY = Math.max(0, (int) cropRect.top);
|
||||||
|
int bmpW = Math.min((int) cropRect.width(), canvasBitmap.getWidth() - bmpX);
|
||||||
|
int bmpH = Math.min((int) cropRect.height(), canvasBitmap.getHeight() - bmpY);
|
||||||
|
if (bmpW > 0 && bmpH > 0) {
|
||||||
|
previewHolder[0] = Bitmap.createBitmap(canvasBitmap, bmpX, bmpY, bmpW, bmpH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final Bitmap previewBitmap = previewHolder[0];
|
||||||
|
|
||||||
|
View dialogView = getLayoutInflater().inflate(R.layout.dialog_crop_info, null);
|
||||||
|
TextView infoText = dialogView.findViewById(R.id.info_text);
|
||||||
|
ImageView previewImage = dialogView.findViewById(R.id.preview_image);
|
||||||
|
final LinearLayout previewImageContainer = dialogView.findViewById(R.id.preview_image_container);
|
||||||
|
infoText.setText(info.toString());
|
||||||
|
if (previewBitmap != null) {
|
||||||
|
previewImage.setImageBitmap(previewBitmap);
|
||||||
|
}
|
||||||
|
previewImage.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
int randomColor = 0xFF000000 | new Random().nextInt(0x00FFFFFF);
|
||||||
|
previewImageContainer.setBackgroundColor(randomColor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||||
|
.setTitle("裁剪信息")
|
||||||
|
.setView(dialogView)
|
||||||
|
.setPositiveButton("确定", null)
|
||||||
|
.create();
|
||||||
|
dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
|
||||||
|
@Override
|
||||||
|
public void onDismiss(DialogInterface dialog) {
|
||||||
|
if (previewBitmap != null && !previewBitmap.isRecycled()) {
|
||||||
|
previewBitmap.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (originalBitmap != null) {
|
||||||
|
originalBitmap.recycle();
|
||||||
|
originalBitmap = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,750 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapShader;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Matrix;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.graphics.Shader;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
public class CropCanvasView extends View {
|
||||||
|
public interface OnBackgroundColorChangedListener {
|
||||||
|
void onBackgroundColorChanged(int color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnColorPickedListener {
|
||||||
|
void onColorPicked(int color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnColorPickEndListener {
|
||||||
|
void onColorPickEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private OnBackgroundColorChangedListener backgroundColorChangedListener;
|
||||||
|
private OnColorPickedListener colorPickedListener;
|
||||||
|
private OnColorPickEndListener colorPickEndListener;
|
||||||
|
|
||||||
|
public void setOnBackgroundColorChangedListener(OnBackgroundColorChangedListener listener) {
|
||||||
|
this.backgroundColorChangedListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnColorPickedListener(OnColorPickedListener listener) {
|
||||||
|
this.colorPickedListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnColorPickEndListener(OnColorPickEndListener listener) {
|
||||||
|
this.colorPickEndListener = listener;
|
||||||
|
}
|
||||||
|
private Paint imagePaint;
|
||||||
|
private Paint borderPaint;
|
||||||
|
private Paint cornerPaint;
|
||||||
|
private RectF cropRect;
|
||||||
|
private int touchArea = 50;
|
||||||
|
|
||||||
|
private float lastX, lastY;
|
||||||
|
private int activeCorner = -1;
|
||||||
|
private static final int CORNER_TOP_LEFT = 0;
|
||||||
|
private static final int CORNER_TOP_RIGHT = 1;
|
||||||
|
private static final int CORNER_BOTTOM_LEFT = 2;
|
||||||
|
private static final int CORNER_BOTTOM_RIGHT = 3;
|
||||||
|
private static final int CORNER_CENTER = 4;
|
||||||
|
|
||||||
|
private int imageWidth;
|
||||||
|
private int imageHeight;
|
||||||
|
private float coverRatio = 2.0f;
|
||||||
|
|
||||||
|
private int extendHeight;
|
||||||
|
private int extendWidth;
|
||||||
|
private int canvasWidth;
|
||||||
|
private int canvasHeight;
|
||||||
|
private float minSize = 50;
|
||||||
|
|
||||||
|
private RectF imageBounds = new RectF();
|
||||||
|
private RectF canvasBounds = new RectF();
|
||||||
|
private Bitmap originalBitmap;
|
||||||
|
private Bitmap canvasBitmap;
|
||||||
|
private float bitmapScale = 1.0f;
|
||||||
|
private Bitmap displayBitmap;
|
||||||
|
private RectF initialSpanRect;
|
||||||
|
private float initialSpan;
|
||||||
|
private int bgType = 2;
|
||||||
|
private Bitmap tileBitmap;
|
||||||
|
private BitmapShader tileShader;
|
||||||
|
private Paint bgPaint;
|
||||||
|
private int backgroundColor = Color.BLUE;
|
||||||
|
private boolean colorPickMode = false;
|
||||||
|
private int previewColor = 0;
|
||||||
|
private float lastPickX = 0;
|
||||||
|
private float lastPickY = 0;
|
||||||
|
private float pickX, pickY;
|
||||||
|
|
||||||
|
private float displayScale = 1.0f;
|
||||||
|
private float displayOffsetX = 0f;
|
||||||
|
private float displayOffsetY = 0f;
|
||||||
|
private static final int MAX_DISPLAY_SIZE = 2048;
|
||||||
|
private float containerScale = 1.0f;
|
||||||
|
|
||||||
|
public CropCanvasView(Context context) {
|
||||||
|
super(context);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CropCanvasView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CropCanvasView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() {
|
||||||
|
imagePaint = new Paint();
|
||||||
|
imagePaint.setFilterBitmap(true);
|
||||||
|
|
||||||
|
borderPaint = new Paint();
|
||||||
|
borderPaint.setColor(Color.parseColor("#CCAA00"));
|
||||||
|
borderPaint.setStyle(Paint.Style.STROKE);
|
||||||
|
borderPaint.setStrokeWidth(3);
|
||||||
|
|
||||||
|
cornerPaint = new Paint();
|
||||||
|
cornerPaint.setColor(Color.WHITE);
|
||||||
|
cornerPaint.setStyle(Paint.Style.FILL);
|
||||||
|
|
||||||
|
bgType = 2;
|
||||||
|
bgPaint = new Paint();
|
||||||
|
initTileBitmap();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initTileBitmap() {
|
||||||
|
if (tileBitmap != null && !tileBitmap.isRecycled()) {
|
||||||
|
tileBitmap.recycle();
|
||||||
|
tileBitmap = null;
|
||||||
|
}
|
||||||
|
tileShader = null;
|
||||||
|
|
||||||
|
if (bgType == 0) {
|
||||||
|
Drawable drawable = getContext().getDrawable(R.drawable.bg_checkerboard);
|
||||||
|
if (drawable != null) {
|
||||||
|
int w = drawable.getIntrinsicWidth();
|
||||||
|
int h = drawable.getIntrinsicHeight();
|
||||||
|
if (w <= 0) w = 10;
|
||||||
|
if (h <= 0) h = 10;
|
||||||
|
tileBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
|
||||||
|
Canvas c = new Canvas(tileBitmap);
|
||||||
|
drawable.setBounds(0, 0, w, h);
|
||||||
|
drawable.draw(c);
|
||||||
|
tileShader = new BitmapShader(tileBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBackgroundType(int type) {
|
||||||
|
if (bgType != type) {
|
||||||
|
bgType = type;
|
||||||
|
initTileBitmap();
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBackgroundType() {
|
||||||
|
return bgType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawBackground(Canvas canvas) {
|
||||||
|
if (bgType == 0 && tileShader != null) {
|
||||||
|
bgPaint.setShader(tileShader);
|
||||||
|
canvas.drawRect(canvasBounds, bgPaint);
|
||||||
|
} else if (bgType == 1) {
|
||||||
|
bgPaint.setShader(null);
|
||||||
|
bgPaint.setColor(Color.WHITE);
|
||||||
|
canvas.drawRect(canvasBounds, bgPaint);
|
||||||
|
} else {
|
||||||
|
bgPaint.setShader(null);
|
||||||
|
bgPaint.setColor(Color.BLACK);
|
||||||
|
canvas.drawRect(canvasBounds, bgPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageBitmap(Bitmap bitmap) {
|
||||||
|
if (displayBitmap != null && displayBitmap != originalBitmap) {
|
||||||
|
displayBitmap.recycle();
|
||||||
|
}
|
||||||
|
this.originalBitmap = bitmap;
|
||||||
|
createDisplayBitmap();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createDisplayBitmap() {
|
||||||
|
if (displayBitmap != null && displayBitmap != originalBitmap) {
|
||||||
|
displayBitmap.recycle();
|
||||||
|
displayBitmap = null;
|
||||||
|
}
|
||||||
|
if (originalBitmap == null || originalBitmap.isRecycled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int w = originalBitmap.getWidth();
|
||||||
|
int h = originalBitmap.getHeight();
|
||||||
|
float scale = 1.0f;
|
||||||
|
if (w > MAX_DISPLAY_SIZE || h > MAX_DISPLAY_SIZE) {
|
||||||
|
scale = Math.min((float) MAX_DISPLAY_SIZE / w, (float) MAX_DISPLAY_SIZE / h);
|
||||||
|
int newW = (int) (w * scale);
|
||||||
|
int newH = (int) (h * scale);
|
||||||
|
displayBitmap = Bitmap.createScaledBitmap(originalBitmap, newW, newH, true);
|
||||||
|
} else {
|
||||||
|
displayBitmap = originalBitmap;
|
||||||
|
}
|
||||||
|
displayBitmapScale = scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float displayBitmapScale = 1.0f;
|
||||||
|
|
||||||
|
public Bitmap getOriginalBitmap() {
|
||||||
|
return originalBitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getDisplayBitmapScale() {
|
||||||
|
return displayBitmapScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onContainerScaled() {
|
||||||
|
containerScale = 1.0f;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContainerScale(float scale) {
|
||||||
|
if (scale > 0 && scale != containerScale) {
|
||||||
|
containerScale = scale;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getContainerScale() {
|
||||||
|
return containerScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bitmap getCanvasBitmap() {
|
||||||
|
if (canvasWidth <= 0 || canvasHeight <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Bitmap canvasBmp = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888);
|
||||||
|
Canvas canvas = new Canvas(canvasBmp);
|
||||||
|
drawBackground(canvas);
|
||||||
|
if (displayBitmap != null && !displayBitmap.isRecycled()) {
|
||||||
|
if (displayBitmap == originalBitmap) {
|
||||||
|
canvas.drawBitmap(displayBitmap, imageBounds.left, imageBounds.top, imagePaint);
|
||||||
|
} else {
|
||||||
|
float invScale = 1f / displayBitmapScale;
|
||||||
|
canvas.save();
|
||||||
|
canvas.scale(invScale, invScale);
|
||||||
|
canvas.drawBitmap(displayBitmap, imageBounds.left * displayBitmapScale,
|
||||||
|
imageBounds.top * displayBitmapScale, imagePaint);
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return canvasBmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initCanvas(int imgWidth, int imgHeight, float ratio) {
|
||||||
|
this.imageWidth = imgWidth;
|
||||||
|
this.imageHeight = imgHeight;
|
||||||
|
this.coverRatio = ratio;
|
||||||
|
|
||||||
|
float imgRatio = (float) imgWidth / imgHeight;
|
||||||
|
|
||||||
|
if (coverRatio >= imgRatio) {
|
||||||
|
extendHeight = imgHeight;
|
||||||
|
extendWidth = (int) (imgHeight * coverRatio);
|
||||||
|
} else {
|
||||||
|
extendWidth = imgWidth;
|
||||||
|
extendHeight = (int) (imgWidth / coverRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasHeight = Math.max(imageHeight, extendHeight);
|
||||||
|
canvasWidth = Math.max(imageWidth, extendWidth);
|
||||||
|
|
||||||
|
float left = (canvasWidth - imageWidth) / 2f;
|
||||||
|
float top = (canvasHeight - imageHeight) / 2f;
|
||||||
|
imageBounds.set(left, top, left + imageWidth, top + imageHeight);
|
||||||
|
canvasBounds.set(0, 0, canvasWidth, canvasHeight);
|
||||||
|
|
||||||
|
cropRect = new RectF(canvasBounds);
|
||||||
|
|
||||||
|
requestLayout();
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getDisplayMatrix(Matrix matrix) {
|
||||||
|
if (canvasWidth <= 0 || canvasHeight <= 0 || getWidth() <= 0 || getHeight() <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
matrix.reset();
|
||||||
|
|
||||||
|
float displayScaleX = (float) getWidth() / canvasWidth;
|
||||||
|
float displayScaleY = (float) getHeight() / canvasHeight;
|
||||||
|
displayScale = Math.min(displayScaleX, displayScaleY);
|
||||||
|
|
||||||
|
displayOffsetX = (getWidth() - canvasWidth * displayScale) / 2f;
|
||||||
|
displayOffsetY = (getHeight() - canvasHeight * displayScale) / 2f;
|
||||||
|
|
||||||
|
matrix.postTranslate(displayOffsetX, displayOffsetY);
|
||||||
|
matrix.postScale(displayScale, displayScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float screenToImageX(float screenX) {
|
||||||
|
return (screenX - displayOffsetX) / displayScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float screenToImageY(float screenY) {
|
||||||
|
return (screenY - displayOffsetY) / displayScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float imageToScreenX(float imageX) {
|
||||||
|
return imageX * displayScale + displayOffsetX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float imageToScreenY(float imageY) {
|
||||||
|
return imageY * displayScale + displayOffsetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getImageColorAt(float screenX, float screenY) {
|
||||||
|
Bitmap bmp = (displayBitmap != null) ? displayBitmap : originalBitmap;
|
||||||
|
if (bmp == null || bmp.isRecycled()) {
|
||||||
|
return Color.TRANSPARENT;
|
||||||
|
}
|
||||||
|
float imgX = screenToImageX(screenX);
|
||||||
|
float imgY = screenToImageY(screenY);
|
||||||
|
int bmpX = (int) ((imgX - imageBounds.left) * displayBitmapScale);
|
||||||
|
int bmpY = (int) ((imgY - imageBounds.top) * displayBitmapScale);
|
||||||
|
if (bmpX >= 0 && bmpX < bmp.getWidth() &&
|
||||||
|
bmpY >= 0 && bmpY < bmp.getHeight()) {
|
||||||
|
return bmp.getPixel(bmpX, bmpY);
|
||||||
|
}
|
||||||
|
return Color.TRANSPARENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBackgroundColor(int color) {
|
||||||
|
this.backgroundColor = color;
|
||||||
|
if (backgroundColorChangedListener != null) {
|
||||||
|
backgroundColorChangedListener.onBackgroundColorChanged(color);
|
||||||
|
}
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBackgroundColor() {
|
||||||
|
return backgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPreviewColor() {
|
||||||
|
return previewColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getLastPickX() {
|
||||||
|
return lastPickX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getLastPickY() {
|
||||||
|
return lastPickY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLastPickImageX() {
|
||||||
|
if (originalBitmap == null || originalBitmap.isRecycled()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
float imgX = screenToImageX(lastPickX);
|
||||||
|
float imgY = screenToImageY(lastPickY);
|
||||||
|
int bmpX = (int) ((imgX - imageBounds.left) * displayBitmapScale);
|
||||||
|
int bmpY = (int) ((imgY - imageBounds.top) * displayBitmapScale);
|
||||||
|
return Math.max(0, Math.min(bmpX, originalBitmap.getWidth() - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLastPickImageY() {
|
||||||
|
if (originalBitmap == null || originalBitmap.isRecycled()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
float imgX = screenToImageX(lastPickX);
|
||||||
|
float imgY = screenToImageY(lastPickY);
|
||||||
|
int bmpX = (int) ((imgX - imageBounds.left) * displayBitmapScale);
|
||||||
|
int bmpY = (int) ((imgY - imageBounds.top) * displayBitmapScale);
|
||||||
|
return Math.max(0, Math.min(bmpY, originalBitmap.getHeight() - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColorPickMode(boolean enable) {
|
||||||
|
this.colorPickMode = enable;
|
||||||
|
if (enable) {
|
||||||
|
pickX = -1;
|
||||||
|
pickY = -1;
|
||||||
|
}
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isColorPickMode() {
|
||||||
|
return colorPickMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void scaleToView(int viewWidth, int viewHeight) {
|
||||||
|
if (viewWidth > 0 && viewHeight > 0 && canvasWidth > 0 && canvasHeight > 0) {
|
||||||
|
float scale = Math.max((float) viewWidth / canvasWidth, (float) viewHeight / canvasHeight);
|
||||||
|
int oldCanvasW = canvasWidth;
|
||||||
|
int oldCanvasH = canvasHeight;
|
||||||
|
canvasWidth = (int) (canvasWidth * scale);
|
||||||
|
canvasHeight = (int) (canvasHeight * scale);
|
||||||
|
|
||||||
|
float left = (canvasWidth - imageWidth) / 2f;
|
||||||
|
float top = (canvasHeight - imageHeight) / 2f;
|
||||||
|
imageBounds.set(left, top, left + imageWidth, top + imageHeight);
|
||||||
|
canvasBounds.set(0, 0, canvasWidth, canvasHeight);
|
||||||
|
|
||||||
|
if (cropRect != null) {
|
||||||
|
float scaleX = (float) canvasWidth / oldCanvasW;
|
||||||
|
float scaleY = (float) canvasHeight / oldCanvasH;
|
||||||
|
cropRect.left *= scaleX;
|
||||||
|
cropRect.top *= scaleY;
|
||||||
|
cropRect.right *= scaleX;
|
||||||
|
cropRect.bottom *= scaleY;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestLayout();
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RectF getCropRect() {
|
||||||
|
return new RectF(cropRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCanvasWidth() {
|
||||||
|
return canvasWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCanvasHeight() {
|
||||||
|
return canvasHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RectF getCanvasBounds() {
|
||||||
|
return new RectF(canvasBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RectF getImageBounds() {
|
||||||
|
return new RectF(imageBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getBitmapScale() {
|
||||||
|
return displayScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getDisplayScale() {
|
||||||
|
return displayScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
int w = canvasWidth > 0 ? canvasWidth : 0;
|
||||||
|
int h = canvasHeight > 0 ? canvasHeight : 0;
|
||||||
|
setMeasuredDimension(w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh);
|
||||||
|
getDisplayMatrix(new Matrix());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
super.onDraw(canvas);
|
||||||
|
|
||||||
|
if (cropRect == null) return;
|
||||||
|
|
||||||
|
if (displayBitmap != null && !displayBitmap.isRecycled()) {
|
||||||
|
canvas.save();
|
||||||
|
|
||||||
|
Matrix matrix = new Matrix();
|
||||||
|
getDisplayMatrix(matrix);
|
||||||
|
canvas.concat(matrix);
|
||||||
|
|
||||||
|
drawBackground(canvas);
|
||||||
|
|
||||||
|
if (displayBitmap == originalBitmap) {
|
||||||
|
canvas.drawBitmap(displayBitmap, imageBounds.left, imageBounds.top, imagePaint);
|
||||||
|
} else {
|
||||||
|
float invScale = 1f / displayBitmapScale;
|
||||||
|
canvas.save();
|
||||||
|
canvas.scale(invScale, invScale);
|
||||||
|
canvas.drawBitmap(displayBitmap, imageBounds.left * displayBitmapScale,
|
||||||
|
imageBounds.top * displayBitmapScale, imagePaint);
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.restore();
|
||||||
|
|
||||||
|
if (containerScale != 1.0f) {
|
||||||
|
canvas.save();
|
||||||
|
canvas.scale(containerScale, containerScale);
|
||||||
|
canvas.drawRect(cropRect, borderPaint);
|
||||||
|
float cornerRadius = 12f / containerScale;
|
||||||
|
canvas.drawCircle(cropRect.left, cropRect.top, cornerRadius, cornerPaint);
|
||||||
|
canvas.drawCircle(cropRect.right, cropRect.top, cornerRadius, cornerPaint);
|
||||||
|
canvas.drawCircle(cropRect.left, cropRect.bottom, cornerRadius, cornerPaint);
|
||||||
|
canvas.drawCircle(cropRect.right, cropRect.bottom, cornerRadius, cornerPaint);
|
||||||
|
canvas.restore();
|
||||||
|
} else {
|
||||||
|
canvas.drawRect(cropRect, borderPaint);
|
||||||
|
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint);
|
||||||
|
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint);
|
||||||
|
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint);
|
||||||
|
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (containerScale != 1.0f) {
|
||||||
|
canvas.save();
|
||||||
|
canvas.scale(containerScale, containerScale);
|
||||||
|
canvas.drawRect(cropRect, borderPaint);
|
||||||
|
float cornerRadius = 12f / containerScale;
|
||||||
|
canvas.drawCircle(cropRect.left, cropRect.top, cornerRadius, cornerPaint);
|
||||||
|
canvas.drawCircle(cropRect.right, cropRect.top, cornerRadius, cornerPaint);
|
||||||
|
canvas.drawCircle(cropRect.left, cropRect.bottom, cornerRadius, cornerPaint);
|
||||||
|
canvas.drawCircle(cropRect.right, cropRect.bottom, cornerRadius, cornerPaint);
|
||||||
|
canvas.restore();
|
||||||
|
} else {
|
||||||
|
canvas.drawRect(cropRect, borderPaint);
|
||||||
|
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint);
|
||||||
|
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint);
|
||||||
|
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint);
|
||||||
|
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
if (cropRect == null || canvasBounds.isEmpty()) return super.onTouchEvent(event);
|
||||||
|
|
||||||
|
if (event.getPointerCount() == 2) {
|
||||||
|
int action = event.getActionMasked();
|
||||||
|
if (action == MotionEvent.ACTION_POINTER_DOWN) {
|
||||||
|
initialSpan = getSpan(event);
|
||||||
|
return true;
|
||||||
|
} else if (action == MotionEvent.ACTION_MOVE) {
|
||||||
|
float span = getSpan(event);
|
||||||
|
if (initialSpan > 0) {
|
||||||
|
float scale = span / initialSpan;
|
||||||
|
float centerX = cropRect.centerX();
|
||||||
|
float centerY = cropRect.centerY();
|
||||||
|
float newWidth = cropRect.width() * scale;
|
||||||
|
float newHeight = newWidth / coverRatio;
|
||||||
|
|
||||||
|
newWidth = Math.max(minSize, Math.min(newWidth, canvasBounds.width()));
|
||||||
|
newHeight = newWidth / coverRatio;
|
||||||
|
|
||||||
|
float newLeft = centerX - newWidth / 2;
|
||||||
|
float newTop = centerY - newHeight / 2;
|
||||||
|
float newRight = centerX + newWidth / 2;
|
||||||
|
float newBottom = centerY + newHeight / 2;
|
||||||
|
|
||||||
|
if (newLeft >= canvasBounds.left && newRight <= canvasBounds.right &&
|
||||||
|
newTop >= canvasBounds.top && newBottom <= canvasBounds.bottom) {
|
||||||
|
cropRect.set(newLeft, newTop, newRight, newBottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialSpan = span;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float x = event.getX();
|
||||||
|
float y = event.getY();
|
||||||
|
|
||||||
|
RectF bounds = canvasBounds;
|
||||||
|
if (containerScale != 1.0f) {
|
||||||
|
x = x / containerScale;
|
||||||
|
y = y / containerScale;
|
||||||
|
bounds = new RectF(
|
||||||
|
canvasBounds.left,
|
||||||
|
canvasBounds.top,
|
||||||
|
canvasBounds.right,
|
||||||
|
canvasBounds.bottom
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colorPickMode) {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) {
|
||||||
|
if (originalBitmap != null && !originalBitmap.isRecycled()) {
|
||||||
|
float imgX = screenToImageX(x);
|
||||||
|
float imgY = screenToImageY(y);
|
||||||
|
if (imageBounds.contains(imgX, imgY)) {
|
||||||
|
previewColor = getImageColorAt(x, y);
|
||||||
|
} else if (canvasBounds.contains(x, y)) {
|
||||||
|
previewColor = (bgType == 1) ? Color.WHITE : Color.BLACK;
|
||||||
|
} else {
|
||||||
|
previewColor = Color.TRANSPARENT;
|
||||||
|
}
|
||||||
|
lastPickX = x;
|
||||||
|
lastPickY = y;
|
||||||
|
if (backgroundColorChangedListener != null) {
|
||||||
|
backgroundColorChangedListener.onBackgroundColorChanged(previewColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||||
|
if (canvasBounds.contains(x, y)) {
|
||||||
|
int pickedColor;
|
||||||
|
float imgX = screenToImageX(x);
|
||||||
|
float imgY = screenToImageY(y);
|
||||||
|
if (imageBounds.contains(imgX, imgY)) {
|
||||||
|
int colorAtPoint = getImageColorAt(x, y);
|
||||||
|
pickedColor = colorAtPoint;
|
||||||
|
backgroundColor = colorAtPoint;
|
||||||
|
} else {
|
||||||
|
pickedColor = (bgType == 1) ? Color.WHITE : Color.BLACK;
|
||||||
|
}
|
||||||
|
if (colorPickedListener != null) {
|
||||||
|
colorPickedListener.onColorPicked(pickedColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (colorPickEndListener != null) {
|
||||||
|
colorPickEndListener.onColorPickEnd();
|
||||||
|
}
|
||||||
|
previewColor = 0;
|
||||||
|
invalidate();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.getAction()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
lastX = x;
|
||||||
|
lastY = y;
|
||||||
|
activeCorner = getActiveCorner(x, y);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
float dx = x - lastX;
|
||||||
|
float dy = y - lastY;
|
||||||
|
|
||||||
|
if (activeCorner == CORNER_CENTER) {
|
||||||
|
float newLeft = cropRect.left + dx;
|
||||||
|
float newTop = cropRect.top + dy;
|
||||||
|
float newRight = cropRect.right + dx;
|
||||||
|
float newBottom = cropRect.bottom + dy;
|
||||||
|
|
||||||
|
if (newLeft >= bounds.left && newRight <= bounds.right) {
|
||||||
|
cropRect.left = newLeft;
|
||||||
|
cropRect.right = newRight;
|
||||||
|
}
|
||||||
|
if (newTop >= bounds.top && newBottom <= bounds.bottom) {
|
||||||
|
cropRect.top = newTop;
|
||||||
|
cropRect.bottom = newBottom;
|
||||||
|
}
|
||||||
|
} else if (activeCorner == CORNER_TOP_LEFT) {
|
||||||
|
adjustCorner(cropRect.left + dx, cropRect.top + dy, true, true);
|
||||||
|
} else if (activeCorner == CORNER_TOP_RIGHT) {
|
||||||
|
adjustCorner(cropRect.right + dx, cropRect.top + dy, false, true);
|
||||||
|
} else if (activeCorner == CORNER_BOTTOM_LEFT) {
|
||||||
|
adjustCorner(cropRect.left + dx, cropRect.bottom + dy, true, false);
|
||||||
|
} else if (activeCorner == CORNER_BOTTOM_RIGHT) {
|
||||||
|
adjustCorner(cropRect.right + dx, cropRect.bottom + dy, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastX = x;
|
||||||
|
lastY = y;
|
||||||
|
invalidate();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
activeCorner = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onTouchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void adjustCorner(float nx, float ny, boolean left, boolean top) {
|
||||||
|
float newWidth;
|
||||||
|
float newLeft = cropRect.left;
|
||||||
|
float newTop = cropRect.top;
|
||||||
|
float newRight = cropRect.right;
|
||||||
|
float newBottom = cropRect.bottom;
|
||||||
|
|
||||||
|
if (left) {
|
||||||
|
newWidth = cropRect.width() - (nx - cropRect.left);
|
||||||
|
newLeft = Math.max(canvasBounds.left, Math.min(nx, cropRect.right - minSize));
|
||||||
|
} else {
|
||||||
|
newWidth = nx - cropRect.left;
|
||||||
|
newRight = Math.min(canvasBounds.right, Math.max(nx, cropRect.left + minSize));
|
||||||
|
newLeft = cropRect.left;
|
||||||
|
}
|
||||||
|
|
||||||
|
float newHeight = newWidth / coverRatio;
|
||||||
|
|
||||||
|
if (top) {
|
||||||
|
newTop = Math.max(canvasBounds.top, Math.min(cropRect.bottom - minSize, cropRect.bottom - newHeight));
|
||||||
|
newBottom = newTop + newHeight;
|
||||||
|
} else {
|
||||||
|
newBottom = Math.min(canvasBounds.bottom, Math.max(cropRect.top + minSize, cropRect.top + newHeight));
|
||||||
|
newTop = newBottom - newHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left) {
|
||||||
|
cropRect.left = newLeft;
|
||||||
|
cropRect.right = newLeft + newWidth;
|
||||||
|
} else {
|
||||||
|
cropRect.right = newRight;
|
||||||
|
cropRect.left = newRight - newWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (top) {
|
||||||
|
cropRect.top = newTop;
|
||||||
|
cropRect.bottom = newTop + newHeight;
|
||||||
|
} else {
|
||||||
|
cropRect.bottom = newBottom;
|
||||||
|
cropRect.top = newBottom - newHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getActiveCorner(float x, float y) {
|
||||||
|
float dx = Math.abs(x - cropRect.left);
|
||||||
|
float dy = Math.abs(y - cropRect.top);
|
||||||
|
float dx2 = Math.abs(x - cropRect.right);
|
||||||
|
float dy2 = Math.abs(y - cropRect.bottom);
|
||||||
|
|
||||||
|
if (dx <= touchArea && dy <= touchArea) {
|
||||||
|
return CORNER_TOP_LEFT;
|
||||||
|
}
|
||||||
|
if (dx2 <= touchArea && dy <= touchArea) {
|
||||||
|
return CORNER_TOP_RIGHT;
|
||||||
|
}
|
||||||
|
if (dx <= touchArea && dy2 <= touchArea) {
|
||||||
|
return CORNER_BOTTOM_LEFT;
|
||||||
|
}
|
||||||
|
if (dx2 <= touchArea && dy2 <= touchArea) {
|
||||||
|
return CORNER_BOTTOM_RIGHT;
|
||||||
|
}
|
||||||
|
if (x > cropRect.left && x < cropRect.right && y > cropRect.top && y < cropRect.bottom) {
|
||||||
|
return CORNER_CENTER;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float getSpan(MotionEvent event) {
|
||||||
|
float x0 = event.getX(0);
|
||||||
|
float y0 = event.getY(0);
|
||||||
|
float x1 = event.getX(1);
|
||||||
|
float y1 = event.getY(1);
|
||||||
|
return (float) Math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getColorAt(float x, float y) {
|
||||||
|
return getImageColorAt(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
public class CropOverlayView extends View {
|
||||||
|
public interface OnCanvasSizeChangedListener {
|
||||||
|
void onCanvasSizeChanged(RectF canvasBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OnCanvasSizeChangedListener canvasSizeChangedListener;
|
||||||
|
|
||||||
|
private Paint borderPaint;
|
||||||
|
private Paint cornerPaint;
|
||||||
|
private RectF cropRect;
|
||||||
|
private int touchArea = 50;
|
||||||
|
|
||||||
|
private float lastX, lastY;
|
||||||
|
private int activeCorner = -1;
|
||||||
|
private static final int CORNER_TOP_LEFT = 0;
|
||||||
|
private static final int CORNER_TOP_RIGHT = 1;
|
||||||
|
private static final int CORNER_BOTTOM_LEFT = 2;
|
||||||
|
private static final int CORNER_BOTTOM_RIGHT = 3;
|
||||||
|
private static final int CORNER_CENTER = 4;
|
||||||
|
|
||||||
|
private float targetRatio = 2.0f;
|
||||||
|
private RectF canvasBounds = new RectF();
|
||||||
|
private float minSize = 50;
|
||||||
|
|
||||||
|
public CropOverlayView(Context context) {
|
||||||
|
super(context);
|
||||||
|
initPaints();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CropOverlayView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
initPaints();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CropOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
initPaints();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initPaints() {
|
||||||
|
borderPaint = new Paint();
|
||||||
|
borderPaint.setColor(Color.parseColor("#CCAA00"));
|
||||||
|
borderPaint.setStyle(Paint.Style.STROKE);
|
||||||
|
borderPaint.setStrokeWidth(3);
|
||||||
|
|
||||||
|
cornerPaint = new Paint();
|
||||||
|
cornerPaint.setColor(Color.WHITE);
|
||||||
|
cornerPaint.setStyle(Paint.Style.FILL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTargetRatio(float ratio) {
|
||||||
|
this.targetRatio = ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getTargetRatio() {
|
||||||
|
return targetRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnCanvasSizeChangedListener(OnCanvasSizeChangedListener listener) {
|
||||||
|
this.canvasSizeChangedListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initCanvas(int canvasW, int canvasH) {
|
||||||
|
canvasBounds.set(0, 0, canvasW, canvasH);
|
||||||
|
|
||||||
|
cropRect = new RectF(0, 0, canvasW, canvasH);
|
||||||
|
|
||||||
|
if (canvasSizeChangedListener != null) {
|
||||||
|
canvasSizeChangedListener.onCanvasSizeChanged(new RectF(canvasBounds));
|
||||||
|
}
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RectF getCropRect() {
|
||||||
|
return new RectF(cropRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RectF getCanvasBounds() {
|
||||||
|
return new RectF(canvasBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
super.onDraw(canvas);
|
||||||
|
|
||||||
|
canvas.drawColor(Color.TRANSPARENT);
|
||||||
|
|
||||||
|
if (cropRect != null) {
|
||||||
|
canvas.drawRect(cropRect, borderPaint);
|
||||||
|
|
||||||
|
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint);
|
||||||
|
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint);
|
||||||
|
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint);
|
||||||
|
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
if (cropRect == null || canvasBounds.isEmpty()) return false;
|
||||||
|
|
||||||
|
float x = event.getX();
|
||||||
|
float y = event.getY();
|
||||||
|
|
||||||
|
switch (event.getAction()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
lastX = x;
|
||||||
|
lastY = y;
|
||||||
|
activeCorner = getActiveCorner(x, y);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
float dx = x - lastX;
|
||||||
|
float dy = y - lastY;
|
||||||
|
|
||||||
|
if (activeCorner == CORNER_CENTER) {
|
||||||
|
float newLeft = cropRect.left + dx;
|
||||||
|
float newTop = cropRect.top + dy;
|
||||||
|
float newRight = cropRect.right + dx;
|
||||||
|
float newBottom = cropRect.bottom + dy;
|
||||||
|
|
||||||
|
if (newLeft >= canvasBounds.left && newRight <= canvasBounds.right) {
|
||||||
|
cropRect.left = newLeft;
|
||||||
|
cropRect.right = newRight;
|
||||||
|
}
|
||||||
|
if (newTop >= canvasBounds.top && newBottom <= canvasBounds.bottom) {
|
||||||
|
cropRect.top = newTop;
|
||||||
|
cropRect.bottom = newBottom;
|
||||||
|
}
|
||||||
|
} else if (activeCorner == CORNER_TOP_LEFT) {
|
||||||
|
adjustCorner(cropRect.left + dx, cropRect.top + dy, true, true);
|
||||||
|
} else if (activeCorner == CORNER_TOP_RIGHT) {
|
||||||
|
adjustCorner(cropRect.right + dx, cropRect.top + dy, false, true);
|
||||||
|
} else if (activeCorner == CORNER_BOTTOM_LEFT) {
|
||||||
|
adjustCorner(cropRect.left + dx, cropRect.bottom + dy, true, false);
|
||||||
|
} else if (activeCorner == CORNER_BOTTOM_RIGHT) {
|
||||||
|
adjustCorner(cropRect.right + dx, cropRect.bottom + dy, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastX = x;
|
||||||
|
lastY = y;
|
||||||
|
invalidate();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
activeCorner = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onTouchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void adjustCorner(float nx, float ny, boolean left, boolean top) {
|
||||||
|
float newWidth;
|
||||||
|
float newLeft = cropRect.left;
|
||||||
|
float newTop = cropRect.top;
|
||||||
|
float newRight = cropRect.right;
|
||||||
|
float newBottom = cropRect.bottom;
|
||||||
|
|
||||||
|
if (left) {
|
||||||
|
newWidth = cropRect.width() - (nx - cropRect.left);
|
||||||
|
newLeft = Math.max(canvasBounds.left, Math.min(nx, cropRect.right - minSize));
|
||||||
|
} else {
|
||||||
|
newWidth = nx - cropRect.left;
|
||||||
|
newRight = Math.min(canvasBounds.right, Math.max(nx, cropRect.left + minSize));
|
||||||
|
newLeft = cropRect.left;
|
||||||
|
}
|
||||||
|
|
||||||
|
float newHeight = newWidth / targetRatio;
|
||||||
|
|
||||||
|
if (top) {
|
||||||
|
newTop = Math.max(canvasBounds.top, Math.min(cropRect.bottom - minSize, cropRect.bottom - newHeight));
|
||||||
|
newBottom = newTop + newHeight;
|
||||||
|
} else {
|
||||||
|
newBottom = Math.min(canvasBounds.bottom, Math.max(cropRect.top + minSize, cropRect.top + newHeight));
|
||||||
|
newTop = newBottom - newHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left) {
|
||||||
|
cropRect.left = newLeft;
|
||||||
|
cropRect.right = newLeft + newWidth;
|
||||||
|
} else {
|
||||||
|
cropRect.right = newRight;
|
||||||
|
cropRect.left = newRight - newWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (top) {
|
||||||
|
cropRect.top = newTop;
|
||||||
|
cropRect.bottom = newTop + newHeight;
|
||||||
|
} else {
|
||||||
|
cropRect.bottom = newBottom;
|
||||||
|
cropRect.top = newBottom - newHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getActiveCorner(float x, float y) {
|
||||||
|
if (Math.abs(x - cropRect.left) <= touchArea && Math.abs(y - cropRect.top) <= touchArea) {
|
||||||
|
return CORNER_TOP_LEFT;
|
||||||
|
}
|
||||||
|
if (Math.abs(x - cropRect.right) <= touchArea && Math.abs(y - cropRect.top) <= touchArea) {
|
||||||
|
return CORNER_TOP_RIGHT;
|
||||||
|
}
|
||||||
|
if (Math.abs(x - cropRect.left) <= touchArea && Math.abs(y - cropRect.bottom) <= touchArea) {
|
||||||
|
return CORNER_BOTTOM_LEFT;
|
||||||
|
}
|
||||||
|
if (Math.abs(x - cropRect.right) <= touchArea && Math.abs(y - cropRect.bottom) <= touchArea) {
|
||||||
|
return CORNER_BOTTOM_RIGHT;
|
||||||
|
}
|
||||||
|
if (cropRect.contains(x, y)) {
|
||||||
|
return CORNER_CENTER;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||||
|
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
|
import cc.winboll.studio.gallery.utils.BackgroundUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/04/24 15:23
|
||||||
|
*/
|
||||||
|
public class GlobalWinBoLLApplication extends GlobalApplication {
|
||||||
|
|
||||||
|
public static final String TAG = "GlobalWinBoLLApplication";
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
LogUtils.d(TAG, "onCreate");
|
||||||
|
setIsDebugging(BuildConfig.DEBUG);
|
||||||
|
//setIsDebugging(false);
|
||||||
|
|
||||||
|
WinBoLLActivityManager.init(this);
|
||||||
|
|
||||||
|
BackgroundUtils.initFromPreferences(this);
|
||||||
|
|
||||||
|
// 初始化 Toast 框架
|
||||||
|
ToastUtils.init(this);
|
||||||
|
// 设置 Toast 布局样式
|
||||||
|
//ToastUtils.setView(R.layout.view_toast);
|
||||||
|
//ToastUtils.setStyle(new WhiteToastStyle());
|
||||||
|
//ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
|
||||||
|
|
||||||
|
//CrashHandler.getInstance().registerGlobal(this);
|
||||||
|
//CrashHandler.getInstance().registerPart(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTerminate() {
|
||||||
|
super.onTerminate();
|
||||||
|
LogUtils.d(TAG, "onTerminate");
|
||||||
|
ToastUtils.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder> {
|
||||||
|
public static final String TAG = "ImageAdapter";
|
||||||
|
private ArrayList<Uri> imageUrls = new ArrayList<>();
|
||||||
|
private ArrayList<String> imagePaths = new ArrayList<>();
|
||||||
|
private OnImageClickListener listener;
|
||||||
|
private int bgType = 0;
|
||||||
|
private Preferences prefs;
|
||||||
|
private PinnedImageDbHelper pinnedDbHelper;
|
||||||
|
private AlbumCoverDbHelper coverDbHelper;
|
||||||
|
private String albumPath;
|
||||||
|
|
||||||
|
private int getBgRes() {
|
||||||
|
switch (bgType) {
|
||||||
|
case 0:
|
||||||
|
return R.drawable.bg_checkerboard;
|
||||||
|
case 1:
|
||||||
|
return R.drawable.bg_white;
|
||||||
|
case 2:
|
||||||
|
return R.drawable.bg_black;
|
||||||
|
default:
|
||||||
|
return R.drawable.bg_checkerboard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnImageClickListener {
|
||||||
|
void onImageClick(int position, ArrayList<Uri> urls, ArrayList<String> paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnImageClickListener(OnImageClickListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setData(ArrayList<Uri> urls, ArrayList<String> paths) {
|
||||||
|
this.imageUrls = urls;
|
||||||
|
this.imagePaths = paths;
|
||||||
|
sortPinnedFirst();
|
||||||
|
LogUtils.d(TAG, "setData: " + urls.size() + " images");
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sortPinnedFirst() {
|
||||||
|
if (pinnedDbHelper == null || imagePaths.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ArrayList<Uri> pinnedUrls = new ArrayList<>();
|
||||||
|
ArrayList<String> pinnedPaths = new ArrayList<>();
|
||||||
|
ArrayList<Uri> unpinnedUrls = new ArrayList<>();
|
||||||
|
ArrayList<String> unpinnedPaths = new ArrayList<>();
|
||||||
|
for (int i = 0; i < imagePaths.size(); i++) {
|
||||||
|
String path = imagePaths.get(i);
|
||||||
|
if (pinnedDbHelper.isPinned(path)) {
|
||||||
|
pinnedUrls.add(imageUrls.get(i));
|
||||||
|
pinnedPaths.add(path);
|
||||||
|
} else {
|
||||||
|
unpinnedUrls.add(imageUrls.get(i));
|
||||||
|
unpinnedPaths.add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imageUrls.clear();
|
||||||
|
imagePaths.clear();
|
||||||
|
imageUrls.addAll(pinnedUrls);
|
||||||
|
imageUrls.addAll(unpinnedUrls);
|
||||||
|
imagePaths.addAll(pinnedPaths);
|
||||||
|
imagePaths.addAll(unpinnedPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContext(android.content.Context context) {
|
||||||
|
prefs = new Preferences(context);
|
||||||
|
bgType = prefs.getBgType();
|
||||||
|
pinnedDbHelper = PinnedImageDbHelper.getInstance(context);
|
||||||
|
coverDbHelper = AlbumCoverDbHelper.getInstance(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlbumPath(String albumPath) {
|
||||||
|
this.albumPath = albumPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCropWidth() {
|
||||||
|
return prefs != null ? prefs.getCoverWidth() : 240;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCropHeight() {
|
||||||
|
return prefs != null ? prefs.getCoverHeight() : 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshBg() {
|
||||||
|
if (prefs != null) {
|
||||||
|
bgType = prefs.getBgType();
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshPinned() {
|
||||||
|
if (pinnedDbHelper == null || imagePaths.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sortPinnedFirst();
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showContextMenu(final View view, final int position) {
|
||||||
|
final String imagePath = imagePaths.get(position);
|
||||||
|
final Uri imageUri = imageUrls.get(position);
|
||||||
|
final boolean[] isPinned = {pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath)};
|
||||||
|
final boolean[] isCover = {false};
|
||||||
|
if (coverDbHelper != null && albumPath != null) {
|
||||||
|
String originalPath = coverDbHelper.getOriginalImagePath(albumPath);
|
||||||
|
isCover[0] = originalPath != null && originalPath.equals(imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext());
|
||||||
|
builder.setTitle("Image");
|
||||||
|
|
||||||
|
String[] items;
|
||||||
|
if (isPinned[0]) {
|
||||||
|
if (isCover[0]) {
|
||||||
|
items = new String[]{"取消置顶", "调整封面", "取消封面"};
|
||||||
|
} else {
|
||||||
|
items = new String[]{"取消置顶", "设置为封面"};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isCover[0]) {
|
||||||
|
items = new String[]{"置顶", "调整封面", "取消封面"};
|
||||||
|
} else {
|
||||||
|
items = new String[]{"置顶", "设置为封面"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setItems(items, new android.content.DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(android.content.DialogInterface dialog, int which) {
|
||||||
|
if (which == 0) {
|
||||||
|
if (pinnedDbHelper != null) {
|
||||||
|
if (isPinned[0]) {
|
||||||
|
pinnedDbHelper.unpinImage(imagePath);
|
||||||
|
} else {
|
||||||
|
pinnedDbHelper.pinImage(imagePath);
|
||||||
|
}
|
||||||
|
refreshPinned();
|
||||||
|
}
|
||||||
|
} else if (which == 1) {
|
||||||
|
if (coverDbHelper != null && albumPath != null) {
|
||||||
|
Intent cropIntent = new Intent(view.getContext(), CropActivity.class);
|
||||||
|
cropIntent.putExtra(CropActivity.EXTRA_IMAGE_URI, imageUri);
|
||||||
|
cropIntent.putExtra(CropActivity.EXTRA_IMAGE_PATH, imagePath);
|
||||||
|
cropIntent.putExtra(CropActivity.EXTRA_ALBUM_PATH, albumPath);
|
||||||
|
cropIntent.putExtra(CropActivity.EXTRA_CROP_WIDTH, getCropWidth());
|
||||||
|
cropIntent.putExtra(CropActivity.EXTRA_CROP_HEIGHT, getCropHeight());
|
||||||
|
((AlbumActivity) view.getContext()).startActivityForResult(cropIntent, 100);
|
||||||
|
}
|
||||||
|
} else if (which == 2) {
|
||||||
|
if (coverDbHelper != null && albumPath != null && isCover[0]) {
|
||||||
|
coverDbHelper.deleteCover(albumPath);
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.item_gallery, parent, false);
|
||||||
|
return new ViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
|
||||||
|
holder.imageView.setBackgroundResource(getBgRes());
|
||||||
|
|
||||||
|
Glide.with(holder.imageView.getContext())
|
||||||
|
.load(imageUrls.get(position))
|
||||||
|
.centerCrop()
|
||||||
|
.into(holder.imageView);
|
||||||
|
|
||||||
|
final ArrayList<Uri> urls = imageUrls;
|
||||||
|
final ArrayList<String> paths = imagePaths;
|
||||||
|
final String imagePath = imagePaths.get(position);
|
||||||
|
|
||||||
|
boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath);
|
||||||
|
holder.pinIcon.setVisibility(isPinned ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
boolean isCover = false;
|
||||||
|
if (coverDbHelper != null && albumPath != null) {
|
||||||
|
String originalPath = coverDbHelper.getOriginalImagePath(albumPath);
|
||||||
|
isCover = originalPath != null && originalPath.equals(imagePath);
|
||||||
|
}
|
||||||
|
holder.coverIcon.setVisibility(isCover ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
holder.itemView.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onImageClick(position, urls, paths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onLongClick(View v) {
|
||||||
|
showContextMenu(holder.itemView, position);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return imageUrls.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
ImageView imageView;
|
||||||
|
ImageView pinIcon;
|
||||||
|
ImageView coverIcon;
|
||||||
|
ViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
imageView = itemView.findViewById(R.id.image);
|
||||||
|
pinIcon = itemView.findViewById(R.id.pin_icon);
|
||||||
|
coverIcon = itemView.findViewById(R.id.cover_icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
public class ImagePagerAdapter extends PagerAdapter {
|
||||||
|
public static final String TAG = "ImagePagerAdapter";
|
||||||
|
private ArrayList<Uri> imageUrls;
|
||||||
|
|
||||||
|
public ImagePagerAdapter(ArrayList<Uri> imageUrls) {
|
||||||
|
this.imageUrls = imageUrls;
|
||||||
|
LogUtils.d(TAG, "ImagePagerAdapter created with " + imageUrls.size() + " images");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
return imageUrls.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||||
|
View view = LayoutInflater.from(container.getContext())
|
||||||
|
.inflate(R.layout.item_image_pager, container, false);
|
||||||
|
view.setBackgroundResource(R.color.black);
|
||||||
|
ImageView imageView = view.findViewById(R.id.image);
|
||||||
|
|
||||||
|
Glide.with(imageView.getContext())
|
||||||
|
.load(imageUrls.get(position))
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.skipMemoryCache(true)
|
||||||
|
.into(imageView);
|
||||||
|
|
||||||
|
container.addView(view);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||||
|
container.removeView((View) object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
|
||||||
|
return view == object;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import android.view.GestureDetector;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.View.OnTouchListener;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.viewpager.widget.ViewPager;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
public class ImageViewerActivity extends Activity implements ViewPager.OnPageChangeListener {
|
||||||
|
public static final String TAG = "ImageViewerActivity";
|
||||||
|
public static final String EXTRA_IMAGE_URLS = "image_urls";
|
||||||
|
public static final String EXTRA_POSITIONS = "image_positions";
|
||||||
|
public static final String EXTRA_POSITION = "position";
|
||||||
|
|
||||||
|
private ArrayList<Uri> imageUrls;
|
||||||
|
private ArrayList<String> imagePaths;
|
||||||
|
private int currentPosition;
|
||||||
|
private ViewPager viewPager;
|
||||||
|
private View toolbar;
|
||||||
|
private ImageButton btnBack;
|
||||||
|
private ImageButton btnDelete;
|
||||||
|
private ImageButton btnShare;
|
||||||
|
private ImageButton btnInfo;
|
||||||
|
private ImageButton btnGallery;
|
||||||
|
private GestureDetector gestureDetector;
|
||||||
|
private TrashManager trashManager;
|
||||||
|
private Preferences prefs;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||||
|
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
|
setContentView(R.layout.activity_image_viewer);
|
||||||
|
LogUtils.d(TAG, "onCreate");
|
||||||
|
|
||||||
|
imageUrls = getIntent().getParcelableArrayListExtra(EXTRA_IMAGE_URLS);
|
||||||
|
imagePaths = getIntent().getStringArrayListExtra(EXTRA_POSITIONS);
|
||||||
|
currentPosition = getIntent().getIntExtra(EXTRA_POSITION, 0);
|
||||||
|
|
||||||
|
trashManager = new TrashManager(this);
|
||||||
|
prefs = new Preferences(this);
|
||||||
|
|
||||||
|
viewPager = findViewById(R.id.view_pager);
|
||||||
|
toolbar = findViewById(R.id.toolbar);
|
||||||
|
btnBack = findViewById(R.id.btn_back);
|
||||||
|
btnDelete = findViewById(R.id.btn_delete);
|
||||||
|
btnShare = findViewById(R.id.btn_share);
|
||||||
|
btnInfo = findViewById(R.id.btn_info);
|
||||||
|
|
||||||
|
btnGallery = findViewById(R.id.btn_gallery);
|
||||||
|
|
||||||
|
ImagePagerAdapter adapter = new ImagePagerAdapter(imageUrls);
|
||||||
|
viewPager.setAdapter(adapter);
|
||||||
|
viewPager.setCurrentItem(currentPosition);
|
||||||
|
viewPager.addOnPageChangeListener(this);
|
||||||
|
|
||||||
|
gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||||
|
toggleToolbar();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
viewPager.setOnTouchListener(new OnTouchListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onTouch(View v, MotionEvent event) {
|
||||||
|
return gestureDetector.onTouchEvent(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btnBack.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btnDelete.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
showDeleteDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btnShare.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
shareCurrentImage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btnInfo.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
showImageInfo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btnGallery.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
Toast.makeText(ImageViewerActivity.this, "Gallery", Toast.LENGTH_SHORT).show();
|
||||||
|
if (imageUrls != null && currentPosition >= 0 && currentPosition < imageUrls.size()) {
|
||||||
|
Uri imageUri = imageUrls.get(currentPosition);
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||||
|
intent.setDataAndType(imageUri, "image/*");
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
startActivity(Intent.createChooser(intent, "打开相册"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleToolbar() {
|
||||||
|
if (toolbar.getVisibility() == View.VISIBLE) {
|
||||||
|
toolbar.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
toolbar.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showDeleteDialog() {
|
||||||
|
new AlertDialog.Builder(this)
|
||||||
|
.setMessage("Delete to trash?")
|
||||||
|
.setPositiveButton("Yes", new android.content.DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(android.content.DialogInterface dialog, int which) {
|
||||||
|
moveToTrash();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton("No", null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveToTrash() {
|
||||||
|
LogUtils.d(TAG, "moveToTrash");
|
||||||
|
if (currentPosition >= 0 && currentPosition < imageUrls.size()) {
|
||||||
|
String imagePath = "";
|
||||||
|
if (imagePaths != null && currentPosition < imagePaths.size()) {
|
||||||
|
imagePath = imagePaths.get(currentPosition);
|
||||||
|
} else {
|
||||||
|
imagePath = getPathFromUri(imageUrls.get(currentPosition));
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri imageUri = imageUrls.get(currentPosition);
|
||||||
|
long result = -1;
|
||||||
|
|
||||||
|
if (!imagePath.isEmpty()) {
|
||||||
|
result = trashManager.addToTrash(imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result > 0) {
|
||||||
|
try {
|
||||||
|
getContentResolver().delete(imageUri, null, null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
android.widget.Toast.makeText(this, "Moved to trash", android.widget.Toast.LENGTH_SHORT).show();
|
||||||
|
LogUtils.i(TAG, "Moved to trash");
|
||||||
|
removeCurrentImage();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
int deleted = getContentResolver().delete(imageUri, null, null);
|
||||||
|
android.widget.Toast.makeText(this, "Deleted: " + deleted, android.widget.Toast.LENGTH_SHORT).show();
|
||||||
|
removeCurrentImage();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
removeCurrentImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeCurrentImage() {
|
||||||
|
imageUrls.remove(currentPosition);
|
||||||
|
if (imagePaths != null) {
|
||||||
|
imagePaths.remove(currentPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageUrls.isEmpty()) {
|
||||||
|
finish();
|
||||||
|
} else {
|
||||||
|
if (currentPosition >= imageUrls.size()) {
|
||||||
|
currentPosition = imageUrls.size() - 1;
|
||||||
|
}
|
||||||
|
viewPager.setAdapter(new ImagePagerAdapter(imageUrls));
|
||||||
|
viewPager.setCurrentItem(currentPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPathFromUri(Uri uri) {
|
||||||
|
String[] projection = { MediaStore.Images.Media.DATA };
|
||||||
|
android.database.Cursor cursor = getContentResolver().query(uri, projection, null, null, null);
|
||||||
|
if (cursor != null) {
|
||||||
|
try {
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
|
||||||
|
return cursor.getString(columnIndex);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uri.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void shareCurrentImage() {
|
||||||
|
if (currentPosition >= 0 && currentPosition < imageUrls.size()) {
|
||||||
|
Uri imageUri = imageUrls.get(currentPosition);
|
||||||
|
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||||
|
shareIntent.setType("image/*");
|
||||||
|
shareIntent.putExtra(Intent.EXTRA_STREAM, imageUri);
|
||||||
|
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
startActivity(Intent.createChooser(shareIntent, "Share Image"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showImageInfo() {
|
||||||
|
if (currentPosition < 0 || currentPosition >= imageUrls.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String imagePath = "";
|
||||||
|
if (imagePaths != null && currentPosition < imagePaths.size()) {
|
||||||
|
imagePath = imagePaths.get(currentPosition);
|
||||||
|
} else {
|
||||||
|
imagePath = getPathFromUri(imageUrls.get(currentPosition));
|
||||||
|
}
|
||||||
|
|
||||||
|
File imageFile = new File(imagePath);
|
||||||
|
if (!imageFile.exists()) {
|
||||||
|
imageFile = new File(imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
android.widget.LinearLayout layout = new android.widget.LinearLayout(this);
|
||||||
|
layout.setOrientation(android.widget.LinearLayout.VERTICAL);
|
||||||
|
layout.setPadding(48, 32, 48, 32);
|
||||||
|
|
||||||
|
android.widget.TextView labelPath = new android.widget.TextView(this);
|
||||||
|
labelPath.setText("Path:");
|
||||||
|
labelPath.setTextColor(getColor(android.R.color.darker_gray));
|
||||||
|
labelPath.setTextSize(14);
|
||||||
|
labelPath.setTypeface(null, android.graphics.Typeface.BOLD);
|
||||||
|
layout.addView(labelPath);
|
||||||
|
|
||||||
|
android.widget.TextView valuePath = new android.widget.TextView(this);
|
||||||
|
valuePath.setText(imagePath);
|
||||||
|
valuePath.setTextColor(getColor(android.R.color.black));
|
||||||
|
valuePath.setTextSize(14);
|
||||||
|
valuePath.setTextIsSelectable(true);
|
||||||
|
layout.addView(valuePath);
|
||||||
|
|
||||||
|
if (imageFile.exists()) {
|
||||||
|
long sizeBytes = imageFile.length();
|
||||||
|
String size;
|
||||||
|
if (sizeBytes < 1024) {
|
||||||
|
size = sizeBytes + " B";
|
||||||
|
} else if (sizeBytes < 1024 * 1024) {
|
||||||
|
size = String.format("%.2f KB", sizeBytes / 1024.0);
|
||||||
|
} else {
|
||||||
|
size = String.format("%.2f MB", sizeBytes / (1024.0 * 1024.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
android.widget.TextView labelSize = new android.widget.TextView(this);
|
||||||
|
labelSize.setText("Size:");
|
||||||
|
labelSize.setTextColor(getColor(android.R.color.darker_gray));
|
||||||
|
labelSize.setTextSize(14);
|
||||||
|
labelSize.setTypeface(null, android.graphics.Typeface.BOLD);
|
||||||
|
layout.addView(labelSize);
|
||||||
|
|
||||||
|
android.widget.TextView valueSize = new android.widget.TextView(this);
|
||||||
|
valueSize.setText(size);
|
||||||
|
valueSize.setTextColor(getColor(android.R.color.black));
|
||||||
|
valueSize.setTextSize(14);
|
||||||
|
valueSize.setTextIsSelectable(true);
|
||||||
|
layout.addView(valueSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
android.graphics.BitmapFactory.Options options = new android.graphics.BitmapFactory.Options();
|
||||||
|
options.inJustDecodeBounds = true;
|
||||||
|
android.graphics.BitmapFactory.decodeFile(imagePath, options);
|
||||||
|
if (options.outWidth > 0 && options.outHeight > 0) {
|
||||||
|
android.widget.TextView labelPixels = new android.widget.TextView(this);
|
||||||
|
labelPixels.setText("Pixels:");
|
||||||
|
labelPixels.setTextColor(getColor(android.R.color.darker_gray));
|
||||||
|
labelPixels.setTextSize(14);
|
||||||
|
labelPixels.setTypeface(null, android.graphics.Typeface.BOLD);
|
||||||
|
layout.addView(labelPixels);
|
||||||
|
|
||||||
|
android.widget.TextView valuePixels = new android.widget.TextView(this);
|
||||||
|
valuePixels.setText(options.outWidth + " x " + options.outHeight);
|
||||||
|
valuePixels.setTextColor(getColor(android.R.color.black));
|
||||||
|
valuePixels.setTextSize(14);
|
||||||
|
valuePixels.setTextIsSelectable(true);
|
||||||
|
layout.addView(valuePixels);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "get pixels error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String[] projection = {
|
||||||
|
MediaStore.Images.Media.DATE_ADDED,
|
||||||
|
MediaStore.Images.Media.DATE_MODIFIED,
|
||||||
|
MediaStore.Images.Media.DATE_TAKEN
|
||||||
|
};
|
||||||
|
android.database.Cursor cursor = getContentResolver().query(
|
||||||
|
imageUrls.get(currentPosition), projection, null, null, null);
|
||||||
|
if (cursor != null) {
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
int dateAddedCol = cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED);
|
||||||
|
int dateTakenCol = cursor.getColumnIndex(MediaStore.Images.Media.DATE_TAKEN);
|
||||||
|
|
||||||
|
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
if (dateTakenCol >= 0) {
|
||||||
|
long dateTaken = cursor.getLong(dateTakenCol);
|
||||||
|
if (dateTaken > 0) {
|
||||||
|
android.widget.TextView labelTaken = new android.widget.TextView(this);
|
||||||
|
labelTaken.setText("Date Taken:");
|
||||||
|
labelTaken.setTextColor(getColor(android.R.color.darker_gray));
|
||||||
|
labelTaken.setTextSize(14);
|
||||||
|
labelTaken.setTypeface(null, android.graphics.Typeface.BOLD);
|
||||||
|
layout.addView(labelTaken);
|
||||||
|
|
||||||
|
android.widget.TextView valueTaken = new android.widget.TextView(this);
|
||||||
|
valueTaken.setText(sdf.format(new java.util.Date(dateTaken)));
|
||||||
|
valueTaken.setTextColor(getColor(android.R.color.black));
|
||||||
|
valueTaken.setTextSize(14);
|
||||||
|
valueTaken.setTextIsSelectable(true);
|
||||||
|
layout.addView(valueTaken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dateAddedCol >= 0) {
|
||||||
|
long dateAdded = cursor.getLong(dateAddedCol);
|
||||||
|
if (dateAdded > 0) {
|
||||||
|
android.widget.TextView labelAdded = new android.widget.TextView(this);
|
||||||
|
labelAdded.setText("Date Added:");
|
||||||
|
labelAdded.setTextColor(getColor(android.R.color.darker_gray));
|
||||||
|
labelAdded.setTextSize(14);
|
||||||
|
labelAdded.setTypeface(null, android.graphics.Typeface.BOLD);
|
||||||
|
layout.addView(labelAdded);
|
||||||
|
|
||||||
|
android.widget.TextView valueAdded = new android.widget.TextView(this);
|
||||||
|
valueAdded.setText(sdf.format(new java.util.Date(dateAdded * 1000)));
|
||||||
|
valueAdded.setTextColor(getColor(android.R.color.black));
|
||||||
|
valueAdded.setTextSize(14);
|
||||||
|
valueAdded.setTextIsSelectable(true);
|
||||||
|
layout.addView(valueAdded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "get date error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
new AlertDialog.Builder(this)
|
||||||
|
.setTitle("Image Info")
|
||||||
|
.setView(layout)
|
||||||
|
.setPositiveButton("OK", null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageSelected(int position) {
|
||||||
|
currentPosition = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageScrollStateChanged(int state) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,419 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.media.MediaScannerConnection;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import android.provider.Settings;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.core.app.ActivityCompat;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import cc.winboll.studio.gallery.AlbumAdapter.OnAlbumClickListener;
|
||||||
|
import cc.winboll.studio.gallery.utils.BackgroundUtils;
|
||||||
|
import cc.winboll.studio.libappbase.LogActivity;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import com.a4455jkjh.colorpicker.ColorPickerDialog;
|
||||||
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileFilter;
|
||||||
|
import java.io.FilenameFilter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
public static final String TAG = "MainActivity";
|
||||||
|
private static final int PERMISSION_REQUEST_CODE = 100;
|
||||||
|
private static final int MANAGE_PERMISSION_REQUEST_CODE = 101;
|
||||||
|
private RecyclerView recyclerView;
|
||||||
|
private AlbumAdapter adapter;
|
||||||
|
private Preferences prefs;
|
||||||
|
private FloatingActionButton fabScrollTop;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_main);
|
||||||
|
LogUtils.d(TAG, "onCreate");
|
||||||
|
|
||||||
|
View content = findViewById(android.R.id.content);
|
||||||
|
if (content != null) {
|
||||||
|
content.setBackground(BackgroundUtils.getInstance().getDrawable());
|
||||||
|
}
|
||||||
|
|
||||||
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
|
||||||
|
prefs = new Preferences(this);
|
||||||
|
|
||||||
|
recyclerView = findViewById(R.id.recycler_view);
|
||||||
|
recyclerView.setLayoutManager(new GridLayoutManager(this, 2));
|
||||||
|
adapter = new AlbumAdapter();
|
||||||
|
adapter.setContext(this);
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
|
fabScrollTop = findViewById(R.id.fab_scroll_top);
|
||||||
|
fabScrollTop.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
recyclerView.scrollToPosition(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy);
|
||||||
|
GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
|
||||||
|
if (layoutManager != null) {
|
||||||
|
int firstVisible = layoutManager.findFirstVisibleItemPosition();
|
||||||
|
if (firstVisible > 0) {
|
||||||
|
fabScrollTop.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
fabScrollTop.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
adapter.setOnAlbumClickListener(new OnAlbumClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onAlbumClick(Album album) {
|
||||||
|
Intent intent = new Intent(MainActivity.this, AlbumActivity.class);
|
||||||
|
intent.putExtra(AlbumActivity.EXTRA_ALBUM_PATH, album.getPath());
|
||||||
|
intent.putExtra(AlbumActivity.EXTRA_ALBUM_NAME, album.getName());
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
checkAndRequestPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkAndRequestPermissions() {
|
||||||
|
LogUtils.i(TAG, "checkAndRequestPermissions");
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
if (!Environment.isExternalStorageManager()) {
|
||||||
|
try {
|
||||||
|
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
|
||||||
|
intent.setData(Uri.parse("package:" + getPackageName()));
|
||||||
|
startActivityForResult(intent, MANAGE_PERMISSION_REQUEST_CODE);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
|
||||||
|
startActivityForResult(intent, MANAGE_PERMISSION_REQUEST_CODE);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkPermission()) {
|
||||||
|
loadAlbums();
|
||||||
|
} else {
|
||||||
|
requestPermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
if (requestCode == MANAGE_PERMISSION_REQUEST_CODE) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
if (Environment.isExternalStorageManager()) {
|
||||||
|
loadAlbums();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, "Permission required", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkPermission() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
return Environment.isExternalStorageManager();
|
||||||
|
}
|
||||||
|
return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
== PackageManager.PERMISSION_GRANTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestPermission() {
|
||||||
|
ActivityCompat.requestPermissions(this,
|
||||||
|
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
|
||||||
|
PERMISSION_REQUEST_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||||
|
@NonNull int[] grantResults) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
if (requestCode == PERMISSION_REQUEST_CODE) {
|
||||||
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
loadAlbums();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadAlbums() {
|
||||||
|
LogUtils.d(TAG, "loadAlbums");
|
||||||
|
String folderPath = prefs.getFolderPath();
|
||||||
|
File baseFolder = new File(folderPath);
|
||||||
|
LogUtils.d(TAG, "baseFolder: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
|
||||||
|
|
||||||
|
if (!baseFolder.exists() || !baseFolder.isDirectory()) {
|
||||||
|
folderPath = Preferences.getDefaultPath();
|
||||||
|
baseFolder = new File(folderPath);
|
||||||
|
LogUtils.d(TAG, "try default: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
|
||||||
|
if (!baseFolder.exists()) {
|
||||||
|
folderPath = Environment.getExternalStorageDirectory() + "/Pictures";
|
||||||
|
baseFolder = new File(folderPath);
|
||||||
|
LogUtils.d(TAG, "try Pictures: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
|
||||||
|
ArrayList<Album> albums = new ArrayList<>();
|
||||||
|
|
||||||
|
FileFilter directoryFilter = new FileFilter() {
|
||||||
|
@Override
|
||||||
|
public boolean accept(File file) {
|
||||||
|
return file.isDirectory();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
File[] subfolders = baseFolder.listFiles(directoryFilter);
|
||||||
|
LogUtils.d(TAG, "subfolders: " + (subfolders != null ? subfolders.length : 0));
|
||||||
|
if (subfolders != null) {
|
||||||
|
for (File subfolder : subfolders) {
|
||||||
|
LogUtils.d(TAG, "scanning folder: " + subfolder.getName());
|
||||||
|
String albumPath = subfolder.getAbsolutePath();
|
||||||
|
String coverPath = coverDbHelper.getCover(albumPath);
|
||||||
|
LogUtils.d(TAG, "loadAlbums: album=" + albumPath + ", coverPath=" + coverPath);
|
||||||
|
Uri coverUri = null;
|
||||||
|
if (coverPath != null) {
|
||||||
|
File coverFile = new File(coverPath);
|
||||||
|
if (coverFile.exists()) {
|
||||||
|
coverUri = Uri.fromFile(coverFile);
|
||||||
|
LogUtils.d(TAG, "loadAlbums: cover from file=" + coverFile.getAbsolutePath());
|
||||||
|
} else {
|
||||||
|
coverUri = getUriFromPath(coverPath);
|
||||||
|
LogUtils.d(TAG, "loadAlbums: cover from media store path=" + coverPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (coverUri == null) {
|
||||||
|
ArrayList<Uri> images = getImagesInFolder(albumPath);
|
||||||
|
if (!images.isEmpty()) {
|
||||||
|
coverUri = images.get(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ArrayList<Uri> allImages = getImagesInFolder(albumPath);
|
||||||
|
if (coverUri != null || !allImages.isEmpty()) {
|
||||||
|
if (coverUri == null && !allImages.isEmpty()) {
|
||||||
|
coverUri = allImages.get(0);
|
||||||
|
}
|
||||||
|
int imageCount = allImages.size();
|
||||||
|
albums.add(new Album(subfolder.getName(), albumPath, coverUri, imageCount));
|
||||||
|
LogUtils.d(TAG, "album added: " + subfolder.getName() + ", " + imageCount + " images");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (albums.isEmpty()) {
|
||||||
|
Toast.makeText(this, R.string.no_images_found, Toast.LENGTH_SHORT).show();
|
||||||
|
LogUtils.i(TAG, "No albums found");
|
||||||
|
}
|
||||||
|
adapter.setData(albums);
|
||||||
|
LogUtils.d(TAG, "Loaded " + albums.size() + " albums");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Uri getUriFromPath(String path) {
|
||||||
|
String[] projection = { MediaStore.Images.Media._ID };
|
||||||
|
String selection = MediaStore.Images.Media.DATA + " = ?";
|
||||||
|
String[] selectionArgs = { path };
|
||||||
|
try (Cursor cursor = getContentResolver().query(
|
||||||
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||||
|
projection, selection, selectionArgs, null)) {
|
||||||
|
if (cursor != null) {
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
|
||||||
|
return Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayList<Uri> getImagesInFolder(String folderPath) {
|
||||||
|
ArrayList<Uri> imageUrls = new ArrayList<>();
|
||||||
|
ContentResolver contentResolver = getContentResolver();
|
||||||
|
Uri collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||||
|
|
||||||
|
String selection = MediaStore.Images.Media.DATA + " LIKE ?";
|
||||||
|
String[] selectionArgs = new String[]{folderPath + "/%"};
|
||||||
|
String sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC";
|
||||||
|
|
||||||
|
LogUtils.d(TAG, "getImagesInFolder: " + folderPath);
|
||||||
|
|
||||||
|
try (Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder)) {
|
||||||
|
if (cursor != null) {
|
||||||
|
LogUtils.d(TAG, "cursor count: " + cursor.getCount());
|
||||||
|
int dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
String path = cursor.getString(dataColumn);
|
||||||
|
if (path != null) {
|
||||||
|
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
|
||||||
|
Uri contentUri = Uri.withAppendedPath(collection, String.valueOf(id));
|
||||||
|
LogUtils.d(TAG, "image: id=" + id + ", path=" + path);
|
||||||
|
imageUrls.add(contentUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogUtils.d(TAG, "found " + imageUrls.size() + " images");
|
||||||
|
return imageUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.menu_main, menu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||||
|
int id = item.getItemId();
|
||||||
|
if (id == R.id.action_mi_gallery) {
|
||||||
|
Toast.makeText(this, "Gallery clicked", Toast.LENGTH_SHORT).show();
|
||||||
|
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||||
|
intent.addCategory(Intent.CATEGORY_APP_GALLERY);
|
||||||
|
startActivity(intent);
|
||||||
|
return true;
|
||||||
|
} else if (id == R.id.action_change_bg_color) {
|
||||||
|
//Toast.makeText(this, "修改背景颜色", Toast.LENGTH_SHORT).show();
|
||||||
|
if (BackgroundUtils.DrawableType.COLOR == BackgroundUtils.getInstance().getDrawableType()) {
|
||||||
|
|
||||||
|
ColorPickerDialog dlg = new ColorPickerDialog(this, BackgroundUtils.getInstance().getColor());
|
||||||
|
dlg.setOnColorChangedListener(new com.a4455jkjh.colorpicker.view.OnColorChangedListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeColorChanged() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onColorChanged(int color) {
|
||||||
|
BackgroundUtils.getInstance().initFromColor(MainActivity.this, color);
|
||||||
|
View content = findViewById(android.R.id.content);
|
||||||
|
if (content != null) {
|
||||||
|
content.setBackground(BackgroundUtils.getInstance().getDrawable());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterColorChanged() {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dlg.show();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else if (id == R.id.action_settings) {
|
||||||
|
startActivity(new Intent(this, SettingsActivity.class));
|
||||||
|
return true;
|
||||||
|
} else if (id == R.id.action_about) {
|
||||||
|
startActivity(new Intent(this, AboutActivity.class));
|
||||||
|
return true;
|
||||||
|
} else if (id == R.id.action_trash) {
|
||||||
|
startActivity(new Intent(this, TrashActivity.class));
|
||||||
|
return true;
|
||||||
|
} else if (id == R.id.action_debug) {
|
||||||
|
LogActivity.startLogActivity(this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BroadcastReceiver coverUpdatedReceiver = new BroadcastReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
if (Preferences.ACTION_COVER_UPDATED.equals(intent.getAction())) {
|
||||||
|
loadAlbums();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
registerReceiver(coverUpdatedReceiver, new IntentFilter(Preferences.ACTION_COVER_UPDATED));
|
||||||
|
if (checkPermission()) {
|
||||||
|
scanMediaStore();
|
||||||
|
loadAlbums();
|
||||||
|
}
|
||||||
|
if (adapter != null) {
|
||||||
|
adapter.refreshBg();
|
||||||
|
adapter.refreshPinned();
|
||||||
|
adapter.refreshCover();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
unregisterReceiver(coverUpdatedReceiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scanMediaStore() {
|
||||||
|
String folderPath = prefs.getFolderPath();
|
||||||
|
File baseFolder = new File(folderPath);
|
||||||
|
if (baseFolder.exists() && baseFolder.isDirectory()) {
|
||||||
|
File[] subfolders = baseFolder.listFiles(new FileFilter() {
|
||||||
|
@Override
|
||||||
|
public boolean accept(File file) {
|
||||||
|
return file.isDirectory();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (subfolders != null) {
|
||||||
|
ArrayList<String> paths = new ArrayList<>();
|
||||||
|
for (File subfolder : subfolders) {
|
||||||
|
File[] images = subfolder.listFiles(new FilenameFilter() {
|
||||||
|
@Override
|
||||||
|
public boolean accept(File dir, String name) {
|
||||||
|
String lower = name.toLowerCase();
|
||||||
|
return lower.endsWith(".jpg") || lower.endsWith(".jpeg")
|
||||||
|
|| lower.endsWith(".png") || lower.endsWith(".gif")
|
||||||
|
|| lower.endsWith(".webp") || lower.endsWith(".bmp");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (images != null) {
|
||||||
|
for (File img : images) {
|
||||||
|
paths.add(img.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!paths.isEmpty()) {
|
||||||
|
LogUtils.d(TAG, "scanning " + paths.size() + " files to MediaStore");
|
||||||
|
String[] pathArray = paths.toArray(new String[0]);
|
||||||
|
MediaScannerConnection.scanFile(this, pathArray, null, new MediaScannerConnection.OnScanCompletedListener() {
|
||||||
|
@Override
|
||||||
|
public void onScanCompleted(String path, Uri uri) {
|
||||||
|
LogUtils.d(TAG, "scanCompleted: " + path + " -> " + uri);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
public class PinnedAlbumDbHelper extends SQLiteOpenHelper {
|
||||||
|
public static final String TAG = "PinnedAlbumDbHelper";
|
||||||
|
private static final String DB_NAME = "pinned_album.db";
|
||||||
|
private static final int DB_VERSION = 1;
|
||||||
|
private static final String TABLE_NAME = "pinned_albums";
|
||||||
|
private static final String COLUMN_PATH = "album_path";
|
||||||
|
|
||||||
|
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
|
||||||
|
+ COLUMN_PATH + " TEXT PRIMARY KEY)";
|
||||||
|
|
||||||
|
private static PinnedAlbumDbHelper dbHelper;
|
||||||
|
|
||||||
|
public static PinnedAlbumDbHelper getInstance(Context context) {
|
||||||
|
if (dbHelper == null) {
|
||||||
|
dbHelper = new PinnedAlbumDbHelper(context.getApplicationContext());
|
||||||
|
}
|
||||||
|
return dbHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PinnedAlbumDbHelper(Context context) {
|
||||||
|
super(context, DB_NAME, null, DB_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(SQLiteDatabase db) {
|
||||||
|
db.execSQL(SQL_CREATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||||
|
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||||
|
onCreate(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pinAlbum(String albumPath) {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(COLUMN_PATH, albumPath);
|
||||||
|
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
|
||||||
|
LogUtils.d(TAG, "pinAlbum: " + albumPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unpinAlbum(String albumPath) {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
db.delete(TABLE_NAME, COLUMN_PATH + " = ?", new String[]{albumPath});
|
||||||
|
LogUtils.d(TAG, "unpinAlbum: " + albumPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPinned(String albumPath) {
|
||||||
|
SQLiteDatabase db = getReadableDatabase();
|
||||||
|
Cursor cursor = db.query(TABLE_NAME, null, COLUMN_PATH + " = ?",
|
||||||
|
new String[]{albumPath}, null, null, null);
|
||||||
|
boolean pinned = cursor.getCount() > 0;
|
||||||
|
cursor.close();
|
||||||
|
return pinned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getPinnedPaths() {
|
||||||
|
SQLiteDatabase db = getReadableDatabase();
|
||||||
|
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH}, null, null, null, null, null);
|
||||||
|
String[] paths = new String[cursor.getCount()];
|
||||||
|
int i = 0;
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
paths[i++] = cursor.getString(0);
|
||||||
|
}
|
||||||
|
cursor.close();
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
public class PinnedImageDbHelper extends SQLiteOpenHelper {
|
||||||
|
public static final String TAG = "PinnedImageDbHelper";
|
||||||
|
private static final String DB_NAME = "pinned_image.db";
|
||||||
|
private static final int DB_VERSION = 1;
|
||||||
|
private static final String TABLE_NAME = "pinned_images";
|
||||||
|
private static final String COLUMN_PATH = "image_path";
|
||||||
|
|
||||||
|
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
|
||||||
|
+ COLUMN_PATH + " TEXT PRIMARY KEY)";
|
||||||
|
|
||||||
|
private static PinnedImageDbHelper dbHelper;
|
||||||
|
|
||||||
|
public static PinnedImageDbHelper getInstance(Context context) {
|
||||||
|
if (dbHelper == null) {
|
||||||
|
dbHelper = new PinnedImageDbHelper(context.getApplicationContext());
|
||||||
|
}
|
||||||
|
return dbHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PinnedImageDbHelper(Context context) {
|
||||||
|
super(context, DB_NAME, null, DB_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(SQLiteDatabase db) {
|
||||||
|
db.execSQL(SQL_CREATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||||
|
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||||
|
onCreate(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pinImage(String imagePath) {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(COLUMN_PATH, imagePath);
|
||||||
|
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
|
||||||
|
LogUtils.d(TAG, "pinImage: " + imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unpinImage(String imagePath) {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
db.delete(TABLE_NAME, COLUMN_PATH + " = ?", new String[]{imagePath});
|
||||||
|
LogUtils.d(TAG, "unpinImage: " + imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPinned(String imagePath) {
|
||||||
|
SQLiteDatabase db = getReadableDatabase();
|
||||||
|
Cursor cursor = db.query(TABLE_NAME, null, COLUMN_PATH + " = ?",
|
||||||
|
new String[]{imagePath}, null, null, null);
|
||||||
|
boolean pinned = cursor.getCount() > 0;
|
||||||
|
cursor.close();
|
||||||
|
return pinned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getPinnedPaths() {
|
||||||
|
SQLiteDatabase db = getReadableDatabase();
|
||||||
|
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH}, null, null, null, null, null);
|
||||||
|
String[] paths = new String[cursor.getCount()];
|
||||||
|
int i = 0;
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
paths[i++] = cursor.getString(0);
|
||||||
|
}
|
||||||
|
cursor.close();
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
public class Preferences {
|
||||||
|
public static final String TAG = "Preferences";
|
||||||
|
private static final String PREF_NAME = "gallery_prefs";
|
||||||
|
private static final String KEY_FOLDER_PATH = "folder_path";
|
||||||
|
private static final String KEY_BG_TYPE = "bg_type";
|
||||||
|
private static final String KEY_ALBUM_SORT_MODE = "album_sort_mode";
|
||||||
|
private static final String KEY_COVER_WIDTH = "cover_width";
|
||||||
|
private static final String KEY_COVER_HEIGHT = "cover_height";
|
||||||
|
private static final String KEY_COVER_RATIO = "cover_ratio";
|
||||||
|
|
||||||
|
public static final String ACTION_COVER_UPDATED = "cc.winboll.studio.gallery.COVER_UPDATED";
|
||||||
|
|
||||||
|
private static final int DEFAULT_BG_TYPE = 0;
|
||||||
|
private static final int DEFAULT_SORT_MODE = 0;
|
||||||
|
private static final int DEFAULT_COVER_WIDTH = 240;
|
||||||
|
private static final int DEFAULT_COVER_HEIGHT = 120;
|
||||||
|
private static final float DEFAULT_COVER_RATIO = 2.0f;
|
||||||
|
private static final String DEFAULT_PATH = "/storage/emulated/0/Pictures/Gallery/owner";
|
||||||
|
|
||||||
|
public static final int SORT_TIME_DESC = 0;
|
||||||
|
public static final int SORT_TIME_ASC = 1;
|
||||||
|
public static final int SORT_NAME_DESC = 2;
|
||||||
|
public static final int SORT_NAME_ASC = 3;
|
||||||
|
|
||||||
|
public static String getDefaultPath() {
|
||||||
|
return DEFAULT_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final SharedPreferences prefs;
|
||||||
|
|
||||||
|
public Preferences(Context context) {
|
||||||
|
prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFolderPath() {
|
||||||
|
String path = prefs.getString(KEY_FOLDER_PATH, DEFAULT_PATH);
|
||||||
|
LogUtils.d(TAG, "getFolderPath: " + path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFolderPath(String path) {
|
||||||
|
LogUtils.d(TAG, "setFolderPath: " + path);
|
||||||
|
prefs.edit().putString(KEY_FOLDER_PATH, path).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBgType() {
|
||||||
|
return prefs.getInt(KEY_BG_TYPE, DEFAULT_BG_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBgType(int type) {
|
||||||
|
LogUtils.d(TAG, "setBgType: " + type);
|
||||||
|
prefs.edit().putInt(KEY_BG_TYPE, type).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAlbumSortMode() {
|
||||||
|
return prefs.getInt(KEY_ALBUM_SORT_MODE, DEFAULT_SORT_MODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlbumSortMode(int mode) {
|
||||||
|
LogUtils.d(TAG, "setAlbumSortMode: " + mode);
|
||||||
|
prefs.edit().putInt(KEY_ALBUM_SORT_MODE, mode).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCoverWidth() {
|
||||||
|
return prefs.getInt(KEY_COVER_WIDTH, DEFAULT_COVER_WIDTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCoverWidth(int width) {
|
||||||
|
prefs.edit().putInt(KEY_COVER_WIDTH, width).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCoverHeight() {
|
||||||
|
return prefs.getInt(KEY_COVER_HEIGHT, DEFAULT_COVER_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCoverHeight(int height) {
|
||||||
|
prefs.edit().putInt(KEY_COVER_HEIGHT, height).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getCoverRatio() {
|
||||||
|
return prefs.getFloat(KEY_COVER_RATIO, DEFAULT_COVER_RATIO);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCoverRatio(float ratio) {
|
||||||
|
prefs.edit().putFloat(KEY_COVER_RATIO, ratio).apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
public class SettingsActivity extends AppCompatActivity {
|
||||||
|
public static final String TAG = "SettingsActivity";
|
||||||
|
private Preferences prefs;
|
||||||
|
private EditText editFolderPath;
|
||||||
|
private TextView textCurrentPath;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_settings);
|
||||||
|
LogUtils.d(TAG, "onCreate");
|
||||||
|
|
||||||
|
prefs = new Preferences(this);
|
||||||
|
|
||||||
|
editFolderPath = findViewById(R.id.edit_folder_path);
|
||||||
|
textCurrentPath = findViewById(R.id.text_current_path);
|
||||||
|
Button btnSave = findViewById(R.id.btn_save);
|
||||||
|
|
||||||
|
String currentPath = prefs.getFolderPath();
|
||||||
|
editFolderPath.setText(currentPath);
|
||||||
|
textCurrentPath.setText("Current: " + currentPath);
|
||||||
|
|
||||||
|
btnSave.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
String newPath = editFolderPath.getText().toString().trim();
|
||||||
|
if (!newPath.isEmpty()) {
|
||||||
|
prefs.setFolderPath(newPath);
|
||||||
|
textCurrentPath.setText("Current: " + newPath);
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.core.app.ActivityCompat;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
public class TrashActivity extends AppCompatActivity {
|
||||||
|
public static final String TAG = "TrashActivity";
|
||||||
|
private static final int PERMISSION_REQUEST_CODE = 102;
|
||||||
|
private RecyclerView recyclerView;
|
||||||
|
private TrashAdapter adapter;
|
||||||
|
private TrashManager trashManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_main);
|
||||||
|
LogUtils.d(TAG, "onCreate");
|
||||||
|
|
||||||
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
getSupportActionBar().setTitle("Trash");
|
||||||
|
|
||||||
|
trashManager = new TrashManager(this);
|
||||||
|
|
||||||
|
recyclerView = findViewById(R.id.recycler_view);
|
||||||
|
recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
|
||||||
|
adapter = new TrashAdapter();
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
|
adapter.setOnTrashClickListener(new TrashAdapter.OnTrashClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onRestoreClick(int position) {
|
||||||
|
restoreImage(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDeleteClick(int position) {
|
||||||
|
permanentlyDelete(position);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (checkPermission()) {
|
||||||
|
loadTrash();
|
||||||
|
} else {
|
||||||
|
requestPermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkPermission() {
|
||||||
|
return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
== PackageManager.PERMISSION_GRANTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestPermission() {
|
||||||
|
ActivityCompat.requestPermissions(this,
|
||||||
|
new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE},
|
||||||
|
PERMISSION_REQUEST_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||||
|
@NonNull int[] grantResults) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
if (requestCode == PERMISSION_REQUEST_CODE) {
|
||||||
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
loadTrash();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadTrash() {
|
||||||
|
LogUtils.d(TAG, "loadTrash");
|
||||||
|
Cursor cursor = trashManager.getTrashList();
|
||||||
|
ArrayList<TrashItem> items = new ArrayList<TrashItem>();
|
||||||
|
ArrayList<Uri> uris = new ArrayList<Uri>();
|
||||||
|
|
||||||
|
String trashPath = TrashDbHelper.getTrashPath();
|
||||||
|
File trashDir = new File(trashPath);
|
||||||
|
|
||||||
|
if (cursor != null && cursor.getCount() > 0) {
|
||||||
|
cursor.moveToFirst();
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
|
||||||
|
String fileName = cursor.getString(cursor.getColumnIndexOrThrow("file_name"));
|
||||||
|
String originalPath = cursor.getString(cursor.getColumnIndexOrThrow("original_path"));
|
||||||
|
String originalFolder = cursor.getString(cursor.getColumnIndexOrThrow("original_folder"));
|
||||||
|
|
||||||
|
TrashItem item = new TrashItem();
|
||||||
|
item.id = id;
|
||||||
|
item.fileName = fileName;
|
||||||
|
item.originalPath = originalPath;
|
||||||
|
item.originalFolder = originalFolder;
|
||||||
|
|
||||||
|
File trashFile = new File(trashDir, fileName);
|
||||||
|
if (trashFile.exists()) {
|
||||||
|
items.add(item);
|
||||||
|
uris.add(Uri.fromFile(trashFile));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
} while (cursor.moveToNext());
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.setData(items, uris);
|
||||||
|
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
Toast.makeText(this, "Trash is empty", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restoreImage(int position) {
|
||||||
|
LogUtils.d(TAG, "restoreImage: " + position);
|
||||||
|
long id = adapter.getItemId(position);
|
||||||
|
String fileName = adapter.getFileName(position);
|
||||||
|
String originalPath = adapter.getOriginalPath(position);
|
||||||
|
|
||||||
|
if (trashManager.restore(id, fileName, originalPath)) {
|
||||||
|
Toast.makeText(this, "Image restored", Toast.LENGTH_SHORT).show();
|
||||||
|
LogUtils.i(TAG, "Image restored");
|
||||||
|
loadTrash();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, "Restore failed", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void permanentlyDelete(int position) {
|
||||||
|
long id = adapter.getItemId(position);
|
||||||
|
String fileName = adapter.getFileName(position);
|
||||||
|
|
||||||
|
if (trashManager.deletePermanently(id, fileName)) {
|
||||||
|
Toast.makeText(this, "Image deleted", Toast.LENGTH_SHORT).show();
|
||||||
|
loadTrash();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, "Delete failed", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.menu_trash, menu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||||
|
if (item.getItemId() == R.id.action_clear_trash) {
|
||||||
|
trashManager.clearTrash();
|
||||||
|
Toast.makeText(this, "Trash cleared", Toast.LENGTH_SHORT).show();
|
||||||
|
loadTrash();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (checkPermission()) {
|
||||||
|
loadTrash();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TrashItem {
|
||||||
|
public long id;
|
||||||
|
public String fileName;
|
||||||
|
public String originalPath;
|
||||||
|
public String originalFolder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
public class TrashAdapter extends RecyclerView.Adapter<TrashAdapter.ViewHolder> {
|
||||||
|
public static final String TAG = "TrashAdapter";
|
||||||
|
private ArrayList<TrashActivity.TrashItem> trashItems = new ArrayList<TrashActivity.TrashItem>();
|
||||||
|
private ArrayList<Uri> imageUrls = new ArrayList<Uri>();
|
||||||
|
private OnTrashClickListener listener;
|
||||||
|
|
||||||
|
public interface OnTrashClickListener {
|
||||||
|
void onRestoreClick(int position);
|
||||||
|
void onDeleteClick(int position);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnTrashClickListener(OnTrashClickListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setData(ArrayList<TrashActivity.TrashItem> items, ArrayList<Uri> uris) {
|
||||||
|
this.trashItems = items;
|
||||||
|
this.imageUrls = uris;
|
||||||
|
LogUtils.d(TAG, "setData: " + items.size() + " items");
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getItemId(int position) {
|
||||||
|
if (position >= 0 && position < trashItems.size()) {
|
||||||
|
return trashItems.get(position).id;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFileName(int position) {
|
||||||
|
if (position >= 0 && position < trashItems.size()) {
|
||||||
|
return trashItems.get(position).fileName;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOriginalPath(int position) {
|
||||||
|
if (position >= 0 && position < trashItems.size()) {
|
||||||
|
return trashItems.get(position).originalPath;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.item_trash, parent, false);
|
||||||
|
return new ViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
|
||||||
|
if (position < imageUrls.size()) {
|
||||||
|
Glide.with(holder.imageView.getContext())
|
||||||
|
.load(imageUrls.get(position))
|
||||||
|
.centerCrop()
|
||||||
|
.into(holder.imageView);
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.btnRestore.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onRestoreClick(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
holder.btnDelete.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onDeleteClick(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return trashItems.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
ImageView imageView;
|
||||||
|
ImageView btnRestore;
|
||||||
|
ImageView btnDelete;
|
||||||
|
ViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
imageView = itemView.findViewById(R.id.image);
|
||||||
|
btnRestore = itemView.findViewById(R.id.btn_restore);
|
||||||
|
btnDelete = itemView.findViewById(R.id.btn_delete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
import android.os.Environment;
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
public class TrashDbHelper extends SQLiteOpenHelper {
|
||||||
|
public static final String TAG = "TrashDbHelper";
|
||||||
|
private static final String DB_NAME = "trash.db";
|
||||||
|
private static final int DB_VERSION = 1;
|
||||||
|
private static final String TABLE_NAME = "trash_items";
|
||||||
|
private static final String COL_ID = "_id";
|
||||||
|
private static final String COL_FILE_NAME = "file_name";
|
||||||
|
private static final String COL_ORIGINAL_PATH = "original_path";
|
||||||
|
private static final String COL_ORIGINAL_FOLDER = "original_folder";
|
||||||
|
private static final String COL_DELETE_TIME = "delete_time";
|
||||||
|
|
||||||
|
public TrashDbHelper(Context context) {
|
||||||
|
super(context, DB_NAME, null, DB_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(SQLiteDatabase db) {
|
||||||
|
LogUtils.d(TAG, "onCreate");
|
||||||
|
db.execSQL("CREATE TABLE " + TABLE_NAME + " (" +
|
||||||
|
COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||||
|
COL_FILE_NAME + " TEXT, " +
|
||||||
|
COL_ORIGINAL_PATH + " TEXT, " +
|
||||||
|
COL_ORIGINAL_FOLDER + " TEXT, " +
|
||||||
|
COL_DELETE_TIME + " INTEGER)");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||||
|
LogUtils.i(TAG, "onUpgrade: " + oldVersion + " -> " + newVersion);
|
||||||
|
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||||
|
onCreate(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long insert(String fileName, String originalPath, String originalFolder) {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(COL_FILE_NAME, fileName);
|
||||||
|
values.put(COL_ORIGINAL_PATH, originalPath);
|
||||||
|
values.put(COL_ORIGINAL_FOLDER, originalFolder);
|
||||||
|
values.put(COL_DELETE_TIME, System.currentTimeMillis());
|
||||||
|
return db.insert(TABLE_NAME, null, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cursor getAll() {
|
||||||
|
SQLiteDatabase db = getReadableDatabase();
|
||||||
|
return db.query(TABLE_NAME, null, null, null, null, null, COL_DELETE_TIME + " DESC");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int delete(long id) {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
return db.delete(TABLE_NAME, COL_ID + "=?", new String[]{String.valueOf(id)});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
db.delete(TABLE_NAME, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTrashPath() {
|
||||||
|
File trashDir = new File(Environment.getExternalStorageDirectory(), ".Trash");
|
||||||
|
if (!trashDir.exists()) {
|
||||||
|
trashDir.mkdirs();
|
||||||
|
}
|
||||||
|
return trashDir.getAbsolutePath();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.media.MediaScannerConnection;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
public class TrashManager {
|
||||||
|
public static final String TAG = "TrashManager";
|
||||||
|
private final Context context;
|
||||||
|
private final TrashDbHelper dbHelper;
|
||||||
|
|
||||||
|
public TrashManager(Context context) {
|
||||||
|
LogUtils.d(TAG, "TrashManager created");
|
||||||
|
this.context = context;
|
||||||
|
this.dbHelper = new TrashDbHelper(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long addToTrash(String imagePath) {
|
||||||
|
LogUtils.d(TAG, "addToTrash: " + imagePath);
|
||||||
|
File sourceFile = new File(imagePath);
|
||||||
|
if (!sourceFile.exists()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
String uniqueId = UUID.randomUUID().toString();
|
||||||
|
String extension = getExtension(imagePath);
|
||||||
|
String newFileName = uniqueId + extension;
|
||||||
|
String trashPath = TrashDbHelper.getTrashPath();
|
||||||
|
File destFile = new File(trashPath, newFileName);
|
||||||
|
|
||||||
|
if (sourceFile.renameTo(destFile)) {
|
||||||
|
String originalFolder = sourceFile.getParent();
|
||||||
|
long result = dbHelper.insert(newFileName, imagePath, originalFolder);
|
||||||
|
LogUtils.i(TAG, "Added to trash: " + newFileName);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
LogUtils.e(TAG, "Failed to move to trash");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cursor getTrashList() {
|
||||||
|
return dbHelper.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean restore(long id, String fileName, String originalPath) {
|
||||||
|
LogUtils.i(TAG, "restore: " + fileName + " -> " + originalPath);
|
||||||
|
File trashFile = new File(TrashDbHelper.getTrashPath(), fileName);
|
||||||
|
LogUtils.d(TAG, "trashFile exists: " + trashFile.exists() + ", path: " + trashFile.getAbsolutePath());
|
||||||
|
if (!trashFile.exists()) {
|
||||||
|
LogUtils.e(TAG, "trashFile not exists: " + trashFile.getAbsolutePath());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
File originalFolder = new File(originalPath).getParentFile();
|
||||||
|
LogUtils.d(TAG, "originalFolder: " + originalFolder + ", exists: " + (originalFolder != null && originalFolder.exists()));
|
||||||
|
if (originalFolder != null && !originalFolder.exists()) {
|
||||||
|
boolean created = originalFolder.mkdirs();
|
||||||
|
LogUtils.d(TAG, "mkdirs result: " + created + ", path: " + originalFolder.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
File originalFile = new File(originalPath);
|
||||||
|
String restoreName = originalFile.getName();
|
||||||
|
File restoreFile = new File(originalFolder, restoreName);
|
||||||
|
LogUtils.d(TAG, "restoreFile: " + restoreFile.getAbsolutePath() + ", exists: " + restoreFile.exists());
|
||||||
|
|
||||||
|
boolean renameResult = trashFile.renameTo(restoreFile);
|
||||||
|
LogUtils.d(TAG, "renameTo result: " + renameResult);
|
||||||
|
|
||||||
|
if (renameResult) {
|
||||||
|
dbHelper.delete(id);
|
||||||
|
LogUtils.i(TAG, "Restored: " + fileName);
|
||||||
|
scanMedia(restoreFile.getAbsolutePath());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try copy + delete if rename failed
|
||||||
|
LogUtils.i(TAG, "renameTo failed, trying copy + delete");
|
||||||
|
try {
|
||||||
|
java.io.InputStream in = new java.io.FileInputStream(trashFile);
|
||||||
|
java.io.OutputStream out = new java.io.FileOutputStream(restoreFile);
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int len;
|
||||||
|
while ((len = in.read(buffer)) > 0) {
|
||||||
|
out.write(buffer, 0, len);
|
||||||
|
}
|
||||||
|
in.close();
|
||||||
|
out.close();
|
||||||
|
boolean deleted = trashFile.delete();
|
||||||
|
LogUtils.d(TAG, "copy+delete result: " + deleted);
|
||||||
|
if (deleted) {
|
||||||
|
dbHelper.delete(id);
|
||||||
|
LogUtils.i(TAG, "Restored (copy): " + fileName);
|
||||||
|
scanMedia(restoreFile.getAbsolutePath());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "copy failed: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
LogUtils.e(TAG, "Failed to restore: " + fileName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean deletePermanently(long id, String fileName) {
|
||||||
|
File trashFile = new File(TrashDbHelper.getTrashPath(), fileName);
|
||||||
|
boolean deleted = trashFile.delete();
|
||||||
|
if (deleted) {
|
||||||
|
dbHelper.delete(id);
|
||||||
|
}
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearTrash() {
|
||||||
|
Cursor cursor = getTrashList();
|
||||||
|
if (cursor != null) {
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
try {
|
||||||
|
int colIndex = cursor.getColumnIndexOrThrow("_id");
|
||||||
|
if (!cursor.isNull(colIndex)) {
|
||||||
|
String fileName = cursor.getString(cursor.getColumnIndexOrThrow("file_name"));
|
||||||
|
File trashFile = new File(TrashDbHelper.getTrashPath(), fileName);
|
||||||
|
trashFile.delete();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
dbHelper.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getExtension(String path) {
|
||||||
|
int lastDot = path.lastIndexOf('.');
|
||||||
|
if (lastDot > 0) {
|
||||||
|
return path.substring(lastDot);
|
||||||
|
}
|
||||||
|
return ".jpg";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scanMedia(String filePath) {
|
||||||
|
LogUtils.d(TAG, "scanMedia: " + filePath);
|
||||||
|
MediaScannerConnection.scanFile(context, new String[]{filePath}, null, new android.media.MediaScannerConnection.OnScanCompletedListener() {
|
||||||
|
@Override
|
||||||
|
public void onScanCompleted(String path, Uri uri) {
|
||||||
|
LogUtils.d(TAG, "scanCompleted: " + path + " -> " + uri);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package cc.winboll.studio.gallery;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.GestureDetector;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.ScaleGestureDetector;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
|
public class ZoomContainerView extends FrameLayout {
|
||||||
|
private float scaleFactor = 1.0f;
|
||||||
|
private float minScale = 0.1f;
|
||||||
|
private float maxScale = 5.0f;
|
||||||
|
private static final float ZOOM_STEP = 0.25f;
|
||||||
|
|
||||||
|
private ScaleGestureDetector scaleGestureDetector;
|
||||||
|
private GestureDetector gestureDetector;
|
||||||
|
private Paint borderPaint;
|
||||||
|
|
||||||
|
public ZoomContainerView(Context context) {
|
||||||
|
super(context);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ZoomContainerView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ZoomContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() {
|
||||||
|
setWillNotDraw(false);
|
||||||
|
setBackgroundColor(Color.YELLOW);
|
||||||
|
|
||||||
|
borderPaint = new Paint();
|
||||||
|
borderPaint.setColor(Color.parseColor("#333333"));
|
||||||
|
borderPaint.setStyle(Paint.Style.STROKE);
|
||||||
|
borderPaint.setStrokeWidth(2);
|
||||||
|
|
||||||
|
scaleGestureDetector = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onScale(ScaleGestureDetector detector) {
|
||||||
|
scaleFactor *= detector.getScaleFactor();
|
||||||
|
scaleFactor = Math.max(minScale, Math.min(scaleFactor, maxScale));
|
||||||
|
invalidate();
|
||||||
|
requestLayout();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onDoubleTap(MotionEvent e) {
|
||||||
|
if (scaleFactor > 1.5f) {
|
||||||
|
scaleFactor = 1.0f;
|
||||||
|
} else {
|
||||||
|
scaleFactor = 2.0f;
|
||||||
|
}
|
||||||
|
invalidate();
|
||||||
|
requestLayout();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void zoomIn() {
|
||||||
|
scaleFactor = Math.min(maxScale, scaleFactor + ZOOM_STEP);
|
||||||
|
requestMeasure();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void zoomOut() {
|
||||||
|
scaleFactor = Math.max(minScale, scaleFactor - ZOOM_STEP);
|
||||||
|
requestMeasure();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestMeasure() {
|
||||||
|
removeCallbacks(measureRunable);
|
||||||
|
post(measureRunable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Runnable measureRunable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
requestLayout();
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public float getScaleFactor() {
|
||||||
|
return scaleFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScaleFactor(float scale) {
|
||||||
|
scaleFactor = Math.max(minScale, Math.min(scale, maxScale));
|
||||||
|
int childCount = getChildCount();
|
||||||
|
if (childCount > 0) {
|
||||||
|
View child = getChildAt(0);
|
||||||
|
if (child instanceof CropCanvasView) {
|
||||||
|
((CropCanvasView) child).setContainerScale(scaleFactor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestLayout();
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetZoom() {
|
||||||
|
scaleFactor = 1.0f;
|
||||||
|
invalidate();
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
int childCount = getChildCount();
|
||||||
|
if (childCount > 0) {
|
||||||
|
View child = getChildAt(0);
|
||||||
|
child.measure(widthMeasureSpec, heightMeasureSpec);
|
||||||
|
int childW = child.getMeasuredWidth();
|
||||||
|
int childH = child.getMeasuredHeight();
|
||||||
|
int scaledW = (int) (childW * scaleFactor);
|
||||||
|
int scaledH = (int) (childH * scaleFactor);
|
||||||
|
int widthSpec = MeasureSpec.makeMeasureSpec(scaledW, MeasureSpec.EXACTLY);
|
||||||
|
int heightSpec = MeasureSpec.makeMeasureSpec(scaledH, MeasureSpec.EXACTLY);
|
||||||
|
child.measure(widthSpec, heightSpec);
|
||||||
|
setMeasuredDimension(scaledW, scaledH);
|
||||||
|
} else {
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||||
|
int childCount = getChildCount();
|
||||||
|
if (childCount > 0) {
|
||||||
|
View child = getChildAt(0);
|
||||||
|
int childW = child.getMeasuredWidth();
|
||||||
|
int childH = child.getMeasuredHeight();
|
||||||
|
int scaledW = (int) (childW * scaleFactor);
|
||||||
|
int scaledH = (int) (childH * scaleFactor);
|
||||||
|
int parentW = right - left;
|
||||||
|
int parentH = bottom - top;
|
||||||
|
int childLeft = (parentW - scaledW) / 2;
|
||||||
|
int childTop = (parentH - scaledH) / 2;
|
||||||
|
child.layout(childLeft, childTop, childLeft + scaledW, childTop + scaledH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
super.onDraw(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
boolean handled = scaleGestureDetector.onTouchEvent(event);
|
||||||
|
if (!scaleGestureDetector.isInProgress()) {
|
||||||
|
handled = gestureDetector.onTouchEvent(event) || handled;
|
||||||
|
}
|
||||||
|
return handled || super.onTouchEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package cc.winboll.studio.gallery.dialog;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import cc.winboll.studio.gallery.R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 颜色表对话框
|
||||||
|
* 继承于普通对话框类,使用视图文件
|
||||||
|
*/
|
||||||
|
public class ColorPaletteDialog extends Dialog {
|
||||||
|
|
||||||
|
public ColorPaletteDialog(@NonNull Context context) {
|
||||||
|
super(context, R.style.ColorPaletteDialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorPaletteDialog(@NonNull Context context, int themeResId) {
|
||||||
|
super(context, themeResId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
// super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.dialog_color_palette);
|
||||||
|
|
||||||
|
TextView titleText = findViewById(R.id.title_text);
|
||||||
|
|
||||||
|
WindowManager.LayoutParams params = getWindow().getAttributes();
|
||||||
|
params.width = WindowManager.LayoutParams.MATCH_PARENT;
|
||||||
|
getWindow().setAttributes(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnColorItemClick {
|
||||||
|
void onColorClick(int color);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package cc.winboll.studio.gallery.utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
import androidx.annotation.DrawableRes;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
public class BackgroundUtils {
|
||||||
|
|
||||||
|
public enum DrawableType {
|
||||||
|
RESOURCE_ID,
|
||||||
|
COLOR
|
||||||
|
}
|
||||||
|
|
||||||
|
private static volatile BackgroundUtils instance;
|
||||||
|
|
||||||
|
private static final String PREF_NAME = "background_prefs";
|
||||||
|
private static final String KEY_TYPE = "bg_type";
|
||||||
|
private static final String KEY_RES_ID = "bg_res_id";
|
||||||
|
private static final String KEY_COLOR = "bg_color";
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
private Drawable drawable;
|
||||||
|
private DrawableType drawableType;
|
||||||
|
private int resId;
|
||||||
|
private int color;
|
||||||
|
|
||||||
|
private BackgroundUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BackgroundUtils getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (BackgroundUtils.class) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new BackgroundUtils();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BackgroundUtils initFromResource(Context context, @DrawableRes int resId) {
|
||||||
|
synchronized (BackgroundUtils.class) {
|
||||||
|
BackgroundUtils utils = getInstance();
|
||||||
|
utils.context = context.getApplicationContext();
|
||||||
|
utils.drawableType = DrawableType.RESOURCE_ID;
|
||||||
|
utils.resId = resId;
|
||||||
|
utils.drawable = ContextCompat.getDrawable(utils.context, resId);
|
||||||
|
utils.saveToPreferences();
|
||||||
|
return utils;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BackgroundUtils initFromColor(Context context, @ColorInt int color) {
|
||||||
|
synchronized (BackgroundUtils.class) {
|
||||||
|
BackgroundUtils utils = getInstance();
|
||||||
|
utils.context = context.getApplicationContext();
|
||||||
|
utils.drawableType = DrawableType.COLOR;
|
||||||
|
utils.color = color;
|
||||||
|
utils.drawable = new ColorDrawable(color);
|
||||||
|
utils.saveToPreferences();
|
||||||
|
return utils;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BackgroundUtils initFromPreferences(Context context) {
|
||||||
|
synchronized (BackgroundUtils.class) {
|
||||||
|
Context appContext = context.getApplicationContext();
|
||||||
|
SharedPreferences prefs = appContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
|
||||||
|
int type = prefs.getInt(KEY_TYPE, -1);
|
||||||
|
if (type == 0) {
|
||||||
|
int resId = prefs.getInt(KEY_RES_ID, 0);
|
||||||
|
if (resId != 0) {
|
||||||
|
return initFromResource(appContext, resId);
|
||||||
|
}
|
||||||
|
} else if (type == 1) {
|
||||||
|
int color = prefs.getInt(KEY_COLOR, Color.BLACK);
|
||||||
|
return initFromColor(appContext, color);
|
||||||
|
}
|
||||||
|
// 默认情况,initFromColor 内部已经调用了 saveToPreferences()
|
||||||
|
return initFromColor(appContext, 0xFF00FF00);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Drawable getDrawable() {
|
||||||
|
return drawable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DrawableType getDrawableType() {
|
||||||
|
return drawableType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DrawableType getAttributeValueType() {
|
||||||
|
return drawableType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getResId() {
|
||||||
|
return resId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getColor() {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveToPreferences() {
|
||||||
|
if (context == null) return;
|
||||||
|
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
|
||||||
|
SharedPreferences.Editor editor = prefs.edit();
|
||||||
|
if (drawableType == DrawableType.RESOURCE_ID) {
|
||||||
|
editor.putInt(KEY_TYPE, 0);
|
||||||
|
editor.putInt(KEY_RES_ID, resId);
|
||||||
|
editor.remove(KEY_COLOR);
|
||||||
|
} else {
|
||||||
|
editor.putInt(KEY_TYPE, 1);
|
||||||
|
editor.putInt(KEY_COLOR, color);
|
||||||
|
editor.remove(KEY_RES_ID);
|
||||||
|
}
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearPreferences(Context context) {
|
||||||
|
SharedPreferences prefs = context.getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
|
||||||
|
prefs.edit().clear().apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package cc.winboll.studio.gallery.utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
import androidx.annotation.DrawableRes;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
public class CropBackgroundUtils {
|
||||||
|
|
||||||
|
public enum DrawableType {
|
||||||
|
RESOURCE_ID,
|
||||||
|
COLOR
|
||||||
|
}
|
||||||
|
|
||||||
|
private static volatile CropBackgroundUtils instance;
|
||||||
|
|
||||||
|
private static final String PREF_NAME = "crop_background_prefs";
|
||||||
|
private static final String KEY_TYPE = "crop_bg_type";
|
||||||
|
private static final String KEY_RES_ID = "crop_bg_res_id";
|
||||||
|
private static final String KEY_COLOR = "crop_bg_color";
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
private Drawable drawable;
|
||||||
|
private DrawableType drawableType;
|
||||||
|
private int resId;
|
||||||
|
private int color;
|
||||||
|
|
||||||
|
private CropBackgroundUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CropBackgroundUtils getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (CropBackgroundUtils.class) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new CropBackgroundUtils();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CropBackgroundUtils initFromResource(Context context, @DrawableRes int resId) {
|
||||||
|
synchronized (CropBackgroundUtils.class) {
|
||||||
|
CropBackgroundUtils utils = getInstance();
|
||||||
|
utils.context = context.getApplicationContext();
|
||||||
|
utils.drawableType = DrawableType.RESOURCE_ID;
|
||||||
|
utils.resId = resId;
|
||||||
|
utils.drawable = ContextCompat.getDrawable(utils.context, resId);
|
||||||
|
utils.saveToPreferences();
|
||||||
|
return utils;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CropBackgroundUtils initFromColor(Context context, @ColorInt int color) {
|
||||||
|
synchronized (CropBackgroundUtils.class) {
|
||||||
|
CropBackgroundUtils utils = getInstance();
|
||||||
|
utils.context = context.getApplicationContext();
|
||||||
|
utils.drawableType = DrawableType.COLOR;
|
||||||
|
utils.color = color;
|
||||||
|
utils.drawable = new ColorDrawable(color);
|
||||||
|
utils.saveToPreferences();
|
||||||
|
return utils;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CropBackgroundUtils initFromPreferences(Context context) {
|
||||||
|
synchronized (CropBackgroundUtils.class) {
|
||||||
|
Context appContext = context.getApplicationContext();
|
||||||
|
SharedPreferences prefs = appContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
|
||||||
|
int type = prefs.getInt(KEY_TYPE, -1);
|
||||||
|
if (type == 0) {
|
||||||
|
int resId = prefs.getInt(KEY_RES_ID, 0);
|
||||||
|
if (resId != 0) {
|
||||||
|
return initFromResource(appContext, resId);
|
||||||
|
}
|
||||||
|
} else if (type == 1) {
|
||||||
|
int color = prefs.getInt(KEY_COLOR, Color.BLACK);
|
||||||
|
return initFromColor(appContext, color);
|
||||||
|
}
|
||||||
|
return initFromColor(appContext, 0xFF00FF00);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Drawable getDrawable() {
|
||||||
|
return drawable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DrawableType getDrawableType() {
|
||||||
|
return drawableType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getResId() {
|
||||||
|
return resId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getColor() {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveToPreferences() {
|
||||||
|
if (context == null) return;
|
||||||
|
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
|
||||||
|
SharedPreferences.Editor editor = prefs.edit();
|
||||||
|
if (drawableType == DrawableType.RESOURCE_ID) {
|
||||||
|
editor.putInt(KEY_TYPE, 0);
|
||||||
|
editor.putInt(KEY_RES_ID, resId);
|
||||||
|
editor.remove(KEY_COLOR);
|
||||||
|
} else {
|
||||||
|
editor.putInt(KEY_TYPE, 1);
|
||||||
|
editor.putInt(KEY_COLOR, color);
|
||||||
|
editor.remove(KEY_RES_ID);
|
||||||
|
}
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearPreferences(Context context) {
|
||||||
|
SharedPreferences prefs = context.getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
|
||||||
|
prefs.edit().clear().apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package cc.winboll.studio.gallery.views;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.widget.RadioButton;
|
||||||
|
|
||||||
|
public class BackgroundRadioButton extends RadioButton {
|
||||||
|
|
||||||
|
CustomApplicationBackground mCustomApplicationBackground;
|
||||||
|
|
||||||
|
public BackgroundRadioButton(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BackgroundRadioButton(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BackgroundRadioButton(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setupCustomApplicationBackground(Context context, int resId) {
|
||||||
|
mCustomApplicationBackground = new CustomApplicationBackground(context, resId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCustomApplicationBackground(CustomApplicationBackground customApplicationBackground) {
|
||||||
|
mCustomApplicationBackground = customApplicationBackground;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CustomApplicationBackground getCustomApplicationBackground() {
|
||||||
|
return mCustomApplicationBackground;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package cc.winboll.studio.gallery.views;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
|
public class CustomApplicationBackground {
|
||||||
|
Drawable mDrawable;
|
||||||
|
|
||||||
|
public CustomApplicationBackground(Drawable drawable) {
|
||||||
|
mDrawable = drawable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CustomApplicationBackground(int color) {
|
||||||
|
mDrawable = new ColorDrawable(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CustomApplicationBackground(Context context, int resId) {
|
||||||
|
mDrawable = context.getDrawable(resId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Drawable getDrawable() {
|
||||||
|
return mDrawable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDrawable(Drawable drawable) {
|
||||||
|
mDrawable = drawable;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
gallery/src/main/res/drawable/bg_black.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#000000"/>
|
||||||
|
</shape>
|
||||||
30
gallery/src/main/res/drawable/bg_checkerboard.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="10dp"
|
||||||
|
android:height="10dp"
|
||||||
|
android:viewportWidth="10"
|
||||||
|
android:viewportHeight="10">
|
||||||
|
<path
|
||||||
|
android:fillColor="#808080"
|
||||||
|
android:pathData="M0,0h1v1h-1z M2,0h1v1h-1z M4,0h1v1h-1z M6,0h1v1h-1z M8,0h1v1h-1z
|
||||||
|
M1,1h1v1h-1z M3,1h1v1h-1z M5,1h1v1h-1z M7,1h1v1h-1z M9,1h1v1h-1z
|
||||||
|
M0,2h1v1h-1z M2,2h1v1h-1z M4,2h1v1h-1z M6,2h1v1h-1z M8,2h1v1h-1z
|
||||||
|
M1,3h1v1h-1z M3,3h1v1h-1z M5,3h1v1h-1z M7,3h1v1h-1z M9,3h1v1h-1z
|
||||||
|
M0,4h1v1h-1z M2,4h1v1h-1z M4,4h1v1h-1z M6,4h1v1h-1z M8,4h1v1h-1z
|
||||||
|
M1,5h1v1h-1z M3,5h1v1h-1z M5,5h1v1h-1z M7,5h1v1h-1z M9,5h1v1h-1z
|
||||||
|
M0,6h1v1h-1z M2,6h1v1h-1z M4,6h1v1h-1z M6,6h1v1h-1z M8,6h1v1h-1z
|
||||||
|
M1,7h1v1h-1z M3,7h1v1h-1z M5,7h1v1h-1z M7,7h1v1h-1z M9,7h1v1h-1z
|
||||||
|
M0,8h1v1h-1z M2,8h1v1h-1z M4,8h1v1h-1z M6,8h1v1h-1z M8,8h1v1h-1z
|
||||||
|
M1,9h1v1h-1z M3,9h1v1h-1z M5,9h1v1h-1z M7,9h1v1h-1z M9,9h1v1h-1z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M1,0h1v1h-1z M3,0h1v1h-1z M5,0h1v1h-1z M7,0h1v1h-1z M9,0h1v1h-1z
|
||||||
|
M0,1h1v1h-1z M2,1h1v1h-1z M4,1h1v1h-1z M6,1h1v1h-1z M8,1h1v1h-1z
|
||||||
|
M1,2h1v1h-1z M3,2h1v1h-1z M5,2h1v1h-1z M7,2h1v1h-1z M9,2h1v1h-1z
|
||||||
|
M0,3h1v1h-1z M2,3h1v1h-1z M4,3h1v1h-1z M6,3h1v1h-1z M8,3h1v1h-1z
|
||||||
|
M1,4h1v1h-1z M3,4h1v1h-1z M5,4h1v1h-1z M7,4h1v1h-1z M9,4h1v1h-1z
|
||||||
|
M0,5h1v1h-1z M2,5h1v1h-1z M4,5h1v1h-1z M6,5h1v1h-1z M8,5h1v1h-1z
|
||||||
|
M1,6h1v1h-1z M3,6h1v1h-1z M5,6h1v1h-1z M7,6h1v1h-1z M9,6h1v1h-1z
|
||||||
|
M0,7h1v1h-1z M2,7h1v1h-1z M4,7h1v1h-1z M6,7h1v1h-1z M8,7h1v1h-1z
|
||||||
|
M1,8h1v1h-1z M3,8h1v1h-1z M5,8h1v1h-1z M7,8h1v1h-1z M9,8h1v1h-1z
|
||||||
|
M0,9h1v1h-1z M2,9h1v1h-1z M4,9h1v1h-1z M6,9h1v1h-1z M8,9h1v1h-1z"/>
|
||||||
|
</vector>
|
||||||
8
gallery/src/main/res/drawable/bg_circle_white.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<solid android:color="#CCFFFFFF"/>
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="#000000"/>
|
||||||
|
</shape>
|
||||||
7
gallery/src/main/res/drawable/bg_color_circle.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<solid android:color="@android:color/transparent"/>
|
||||||
|
<stroke android:width="2dp" android:color="@android:color/white"/>
|
||||||
|
<size android:width="32dp" android:height="32dp"/>
|
||||||
|
</shape>
|
||||||
6
gallery/src/main/res/drawable/bg_color_circle_border.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<stroke android:width="2dp" android:color="@android:color/white"/>
|
||||||
|
<size android:width="36dp" android:height="36dp"/>
|
||||||
|
</shape>
|
||||||
6
gallery/src/main/res/drawable/bg_dialog.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="@android:color/black"/>
|
||||||
|
<stroke android:width="2dp" android:color="@android:color/white"/>
|
||||||
|
</shape>
|
||||||
5
gallery/src/main/res/drawable/bg_white.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#FFFFFF"/>
|
||||||
|
</shape>
|
||||||
10
gallery/src/main/res/drawable/ic_arrow_up.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z"/>
|
||||||
|
</vector>
|
||||||
9
gallery/src/main/res/drawable/ic_back.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||||
|
</vector>
|
||||||
12
gallery/src/main/res/drawable/ic_bg.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M4,4h7v7h-7z M13,4h7v7h-7z M4,13h7v7h-7z M13,13h7v7h-7z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#808080"
|
||||||
|
android:pathData="M11,4h2v7h-2z M4,11h7v2h-7z M13,11h7v2h-7z M11,13h2v7h-2z M13,4h2v7h-2z M4,13h7v2h-7z"/>
|
||||||
|
</vector>
|
||||||
10
gallery/src/main/res/drawable/ic_close.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||||
|
</vector>
|
||||||
10
gallery/src/main/res/drawable/ic_color_pick.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M20.71,5.63l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-3.12,3.12 -1.93,-1.91 -1.41,1.41 1.42,1.42L3,16.25V21h4.75l8.92,-8.92 1.42,1.42 1.41,-1.41 -1.92,-1.92 3.12,-3.12c0.4,-0.4 0.4,-1.03 0.01,-1.42zM6.92,19H5v-1.92l8.06,-8.06 1.92,1.92L6.92,19z"/>
|
||||||
|
</vector>
|
||||||
13
gallery/src/main/res/drawable/ic_cover.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M21,3H3C1.9,3 1,3.9 1,5v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V5C23,3.9 22.1,3 21,3zM21,19H3V5h18V19z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M9,12l2,2l4,-4l1.5,1.5L11,17l-3,-3z"/>
|
||||||
|
</vector>
|
||||||
9
gallery/src/main/res/drawable/ic_delete.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,-1H5v2h14V4z"/>
|
||||||
|
</vector>
|
||||||
9
gallery/src/main/res/drawable/ic_done.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||||
|
</vector>
|
||||||
9
gallery/src/main/res/drawable/ic_info.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
|
||||||
|
</vector>
|
||||||
16
gallery/src/main/res/drawable/ic_mi_gallery.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8s8,3.59 8,8s-3.59,8 -8,8z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5s5,-2.24 5,-5s-2.24,-5 -5,-5zM12,15c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3s3,1.34 3,3s-1.34,3 -3,3z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M8,12l1.5,-2l2,2.5l2.5,-3l2,2.5"/>
|
||||||
|
</vector>
|
||||||
9
gallery/src/main/res/drawable/ic_pin.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M16,12V4H17V2H7V4H8V12L6,14V16H11.5V22H12.5V16H18V14L16,12Z"/>
|
||||||
|
</vector>
|
||||||
9
gallery/src/main/res/drawable/ic_restore.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9H1l3.89,3.89 0.07,0.14L9,12H6c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,17.9 10.51,19 13,19c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08V8H12z"/>
|
||||||
|
</vector>
|
||||||
9
gallery/src/main/res/drawable/ic_share.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92s2.92,-1.31 2.92,-2.92 -1.31,-2.92 -2.92,-2.92z"/>
|
||||||
|
</vector>
|
||||||
10
gallery/src/main/res/drawable/ic_sort.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M3,18h6v-2H3v2zM3,6v2h18V6H3zM3,13h12v-2H3v2z"/>
|
||||||
|
</vector>
|
||||||
11
gallery/src/main/res/drawable/ic_view_gallery_outline.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:viewportWidth="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M1,3V21H23V3H1M21,5V14H3V5H21M11,16V19H8V16H11M3,16H6V19H3V16M13,19V16H16V19H13M18,19V16H21V19H18Z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
9
gallery/src/main/res/drawable/ic_zoom_in.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||||
|
</vector>
|
||||||
9
gallery/src/main/res/drawable/ic_zoom_out.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M19,13H5v-2h14v2z"/>
|
||||||
|
</vector>
|
||||||
33
gallery/src/main/res/layout/activity_about.xml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<cc.winboll.studio.libappbase.views.AboutView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:id="@+id/aboutview"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
128
gallery/src/main/res/layout/activity_crop.xml
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:background="@color/colorPrimary"
|
||||||
|
android:paddingHorizontal="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/btn_close"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:padding="5dp"
|
||||||
|
android:src="@drawable/ic_close"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textview_window_name"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="封面剪裁"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginEnd="8dp"/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/btn_info"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:padding="5dp"
|
||||||
|
android:src="@drawable/ic_info"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/btn_change_bg"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:padding="5dp"
|
||||||
|
android:src="@drawable/ic_color_pick"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="修改剪裁背景颜色"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/btn_done"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:padding="5dp"
|
||||||
|
android:src="@drawable/ic_done"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/scroll_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="56dp"
|
||||||
|
android:layout_marginBottom="56dp"
|
||||||
|
android:fillViewport="true">
|
||||||
|
|
||||||
|
<cc.winboll.studio.gallery.ZoomContainerView
|
||||||
|
android:id="@+id/zoom_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
|
||||||
|
<cc.winboll.studio.gallery.CropCanvasView
|
||||||
|
android:id="@+id/crop_canvas_view"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"/>
|
||||||
|
|
||||||
|
</cc.winboll.studio.gallery.ZoomContainerView>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:background="@color/colorPrimary"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
android:layout_marginBottom="0dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="缩小"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="12sp"/>
|
||||||
|
|
||||||
|
<SeekBar
|
||||||
|
android:id="@+id/seekbar_zoom"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:min="0"
|
||||||
|
android:max="100"
|
||||||
|
android:progress="50"
|
||||||
|
android:layout_marginHorizontal="8dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="放大"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="12sp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
73
gallery/src/main/res/layout/activity_image_viewer.xml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true">
|
||||||
|
|
||||||
|
<androidx.viewpager.widget.ViewPager
|
||||||
|
android:id="@+id/view_pager"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clickable="false"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="top"
|
||||||
|
android:background="#CC000000"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="4dp">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_back"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_back"
|
||||||
|
android:contentDescription="Back"/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_gallery"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_view_gallery_outline"
|
||||||
|
android:contentDescription="Gallery"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_share"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_share"
|
||||||
|
android:contentDescription="Share"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_info"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_info"
|
||||||
|
android:contentDescription="Info"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_delete"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_delete"
|
||||||
|
android:contentDescription="Delete"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
45
gallery/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@android:color/transparent"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/fab_scroll_top"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:src="@drawable/ic_arrow_up"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:backgroundTint="@color/colorAccent"
|
||||||
|
app:tint="@color/white"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
39
gallery/src/main/res/layout/activity_settings.xml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/folder_path"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_folder_path"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:hint="@string/enter_folder_path"
|
||||||
|
android:inputType="text"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_current_path"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="#888888"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_save"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/save"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
16
gallery/src/main/res/layout/dialog_color_palette.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/bg_dialog"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textStyle="bold"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
36
gallery/src/main/res/layout/dialog_crop_info.xml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="@drawable/bg_dialog"
|
||||||
|
android:gravity="center_horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/info_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textColor="@android:color/white"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/preview_image_container"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="2dp"
|
||||||
|
android:background="#FFBBD505">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/preview_image"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:scaleType="fitCenter"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
48
gallery/src/main/res/layout/item_album.xml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="2dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/album_cover"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="120dp"
|
||||||
|
android:layout_margin="2dp"
|
||||||
|
android:scaleType="centerCrop"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/album_name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:background="#80000000"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pin_icon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_margin="6dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:padding="3dp"
|
||||||
|
android:src="@drawable/ic_pin"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/image_count"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="top|end"
|
||||||
|
android:background="#80000000"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="10sp"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
20
gallery/src/main/res/layout/item_color_icon.xml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/color_view"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="@drawable/bg_color_circle"/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="@drawable/bg_color_circle_border"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
41
gallery/src/main/res/layout/item_gallery.xml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="2dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="120dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:background="@color/black"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pin_icon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_gravity="top|end"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
android:background="@drawable/bg_circle_white"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:src="@drawable/ic_pin"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:tint="@color/black"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/cover_icon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
android:background="@drawable/bg_circle_white"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:src="@drawable/ic_cover"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:tint="@color/black"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
13
gallery/src/main/res/layout/item_image_pager.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="fitCenter"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
42
gallery/src/main/res/layout/item_trash.xml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="2dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="120dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:background="@color/black"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:background="#80000000"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/btn_restore"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:src="@drawable/ic_restore"
|
||||||
|
android:contentDescription="Restore"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/btn_delete"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:src="@drawable/ic_delete"
|
||||||
|
android:contentDescription="Delete"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
26
gallery/src/main/res/menu/menu_album.xml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_sort"
|
||||||
|
android:icon="@drawable/ic_sort"
|
||||||
|
android:title="排序"
|
||||||
|
app:showAsAction="ifRoom">
|
||||||
|
<menu>
|
||||||
|
<group android:checkableBehavior="single">
|
||||||
|
<item
|
||||||
|
android:id="@+id/sort_time_desc"
|
||||||
|
android:title="时间倒序"/>
|
||||||
|
<item
|
||||||
|
android:id="@+id/sort_time_asc"
|
||||||
|
android:title="时间正序"/>
|
||||||
|
<item
|
||||||
|
android:id="@+id/sort_name_desc"
|
||||||
|
android:title="名称倒序"/>
|
||||||
|
<item
|
||||||
|
android:id="@+id/sort_name_asc"
|
||||||
|
android:title="名称正序"/>
|
||||||
|
</group>
|
||||||
|
</menu>
|
||||||
|
</item>
|
||||||
|
</menu>
|
||||||
8
gallery/src/main/res/menu/menu_crop.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_done"
|
||||||
|
android:title="完成"
|
||||||
|
app:showAsAction="always"/>
|
||||||
|
</menu>
|
||||||
36
gallery/src/main/res/menu/menu_main.xml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_mi_gallery"
|
||||||
|
android:title="@string/mi_gallery"
|
||||||
|
android:icon="@drawable/ic_mi_gallery"
|
||||||
|
app:showAsAction="ifRoom"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_change_bg_color"
|
||||||
|
android:title="修改背景颜色"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_trash"
|
||||||
|
android:title="@string/trash"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_settings"
|
||||||
|
android:title="@string/settings"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_about"
|
||||||
|
android:title="@string/about"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_debug"
|
||||||
|
android:title="@string/debug_log"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
</menu>
|
||||||
15
gallery/src/main/res/menu/menu_trash.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_trash"
|
||||||
|
android:title="@string/trash"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_clear_trash"
|
||||||
|
android:title="@string/clear_trash"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
</menu>
|
||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -3,4 +3,6 @@
|
|||||||
<color name="colorPrimary">#009688</color>
|
<color name="colorPrimary">#009688</color>
|
||||||
<color name="colorPrimaryDark">#00796B</color>
|
<color name="colorPrimaryDark">#00796B</color>
|
||||||
<color name="colorAccent">#FF9800</color>
|
<color name="colorAccent">#FF9800</color>
|
||||||
|
<color name="black">#000000</color>
|
||||||
|
<color name="white">#FFFFFF</color>
|
||||||
</resources>
|
</resources>
|
||||||
24
gallery/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">Gallery</string>
|
||||||
|
<string name="app_description">WinBoLL Album App</string>
|
||||||
|
<string name="refresh">Refresh</string>
|
||||||
|
<string name="settings">Settings</string>
|
||||||
|
<string name="settings_title">Settings</string>
|
||||||
|
<string name="about">About</string>
|
||||||
|
<string name="folder_path">Folder Path</string>
|
||||||
|
<string name="enter_folder_path">Enter folder path</string>
|
||||||
|
<string name="save">Save</string>
|
||||||
|
<string name="cancel">Cancel</string>
|
||||||
|
<string name="no_images_found">No images found</string>
|
||||||
|
<string name="trash">Trash</string>
|
||||||
|
<string name="clear_trash">Clear Trash</string>
|
||||||
|
<string name="delete_confirm">Delete to trash?</string>
|
||||||
|
<string name="restore_confirm">Restore to original folder?</string>
|
||||||
|
<string name="yes">Yes</string>
|
||||||
|
<string name="no">No</string>
|
||||||
|
<string name="debug_log">Debug Log</string>
|
||||||
|
<string name="debug_message">Debug log message</string>
|
||||||
|
<string name="system_gallery">系统相册</string>
|
||||||
|
<string name="mi_gallery">小米相册</string>
|
||||||
|
<string name="reset_gallery">重置</string>
|
||||||
|
</resources>
|
||||||