mirror of
https://gitea.winboll.cc/ZhanGSKen/MyWinBoLL.git
synced 2026-08-14 21:47:11 +08:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c348db4f7 | |||
| a9f3c9fa81 | |||
| ce01ecf138 | |||
| 9bc9cf3700 | |||
| e97c5fe050 | |||
| 14ed5d60c5 | |||
| dbd10b88d9 | |||
| a4acdce1de | |||
| 507d614602 | |||
| 261ae68193 | |||
| eed003f590 | |||
| 01b2d4a4c3 | |||
| 394efd3892 | |||
| 184d4eef64 | |||
| 975af960a4 | |||
| 2619974da6 | |||
| e136045d1f | |||
| bd635425a1 | |||
| 49414a3938 | |||
| 08a47768a1 | |||
| 79f161b559 | |||
| b14538db47 | |||
| c9e16a9a1e | |||
| 42dbfb4238 | |||
| b274503187 | |||
| b50e4e02ea | |||
| 84f1c820cb | |||
| 0e60d75ac6 | |||
| 773cc9f6c1 | |||
| ca33dab4da | |||
| eee8809485 | |||
| 69f79549e9 | |||
| f204cd2608 | |||
| 2d3592690a | |||
| 576f8bb5e5 | |||
| e787697d6b | |||
| 3db3edcf2c | |||
| 643d08bf22 | |||
| beefb16c32 | |||
| 878009ec9f | |||
| 2d8b5d515b | |||
| 1453287667 | |||
| 5fc59f3305 | |||
| b6ba73126f | |||
| bdd8d1e2b6 | |||
| 138ca80e35 | |||
| 7a3b54b78b | |||
| db804d1897 | |||
| 039c8fcd98 | |||
| ae63d1ec0a | |||
| 9e9486b488 | |||
| 4b8967b253 | |||
| b1059c3f46 | |||
| f3d723fbee | |||
| e3c30ea9a3 | |||
| ab4cef21f0 | |||
| 2754a2ad7c |
+2
-2
@@ -94,8 +94,8 @@ lint-results.html
|
||||
## 忽略 AndroidIDE 临时文件夹
|
||||
.androidide
|
||||
|
||||
## WinBoLL 基础应用(避免上传敏感配置)
|
||||
## MyWinBoLL 基础应用(避免上传敏感配置)
|
||||
/winboll.properties
|
||||
/local.properties
|
||||
#/settings.gradle
|
||||
#/gradle.properties
|
||||
#/gradle.properties
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/system/bin/sh
|
||||
## 合并其他项目分支的模块源码到projects-keeper分支。
|
||||
## 合并其他项目分支的模块源码到projects_keeper分支。
|
||||
|
||||
# ====================== 0. 进入目标目录 ======================
|
||||
TARGET_DIR="/sdcard/AppProjects/Projects_Keeper"
|
||||
@@ -36,7 +36,7 @@ fi
|
||||
|
||||
# ====================== 3. Git 分支检查 ======================
|
||||
CUR_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
|
||||
TARGET_BRANCH="projects-keeper"
|
||||
TARGET_BRANCH="projects_keeper"
|
||||
|
||||
if [ "$CUR_BRANCH" != "$TARGET_BRANCH" ]; then
|
||||
echo "错误:当前不在 $TARGET_BRANCH 分支!"
|
||||
@@ -72,7 +72,6 @@ libaes
|
||||
libappbase
|
||||
libdebugtemp
|
||||
libgpsrelaysentinel
|
||||
libwinboll
|
||||
local.properties-demo
|
||||
mymessagemanager
|
||||
positions
|
||||
@@ -82,13 +81,14 @@ winboll
|
||||
winboll.properties-demo
|
||||
)
|
||||
|
||||
# ====================== 5. 获取当前目录真实文件列表 ======================
|
||||
# ====================== 5. 获取当前目录真实文件列表(兼容过滤 . ..) ======================
|
||||
REAL_ITEMS=()
|
||||
# 使用固定排序ls,自动过滤 . 和 ..,不会进入比对数组
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" != "." && "$line" != ".." ]]; then
|
||||
REAL_ITEMS+=("$line")
|
||||
fi
|
||||
done < <(ls -a)
|
||||
done < <(LC_COLLATE=C ls -a1 --color=none)
|
||||
|
||||
# ====================== 6. 差异比对函数 ======================
|
||||
check_diff() {
|
||||
@@ -158,7 +158,7 @@ echo -e "## 对象列表结束
|
||||
|
||||
## 合并 APP 项目
|
||||
MERGE_APP_PROJECT_LIST=(
|
||||
DemoAPP
|
||||
WinBoLL
|
||||
)
|
||||
echo -e "#@@@ 开始合并应用型模块源码 @@@#
|
||||
## 目标合并对象列表:"
|
||||
@@ -166,14 +166,13 @@ echo -e "#@@@ 开始合并应用型模块源码 @@@#
|
||||
for item in "${MERGE_APP_PROJECT_LIST[@]}"; do
|
||||
echo "正在合并 $item 项目 ..."
|
||||
item_lower=$(echo "$item" | tr 'A-Z' 'a-z')
|
||||
git checkout origin/$item_lower $item_lower
|
||||
git checkout origin/$item_lower $item_lower
|
||||
git add .
|
||||
git commit -m "合并 $item 项目"
|
||||
done
|
||||
|
||||
## 合并 LIB 项目
|
||||
MERGE_LIB_PROJECT_LIST=(
|
||||
WinBoLL
|
||||
APPBase
|
||||
AES
|
||||
)
|
||||
@@ -183,10 +182,10 @@ echo -e "#@@@ 开始合并类库型模块源码 @@@#
|
||||
for item in "${MERGE_LIB_PROJECT_LIST[@]}"; do
|
||||
echo "正在合并 $item 项目 ..."
|
||||
item_lower=$(echo "$item" | tr 'A-Z' 'a-z')
|
||||
git checkout origin/$item_lower $item_lower lib$item_lower
|
||||
git checkout origin/$item_lower $item_lower lib$item_lower
|
||||
git add .
|
||||
git commit -m "合并 $item 项目"
|
||||
done
|
||||
|
||||
echo '正在推送 Projects_Keeper 项目'
|
||||
git push
|
||||
git push
|
||||
|
||||
@@ -38,7 +38,7 @@ if [ "${NOW_BRANCH}" != "${TARGET_BRANCH}" ];then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 目录结构校验
|
||||
# 目录结构校验白名单(不含 . ..)
|
||||
MERGE_OBJECTS_LIST=(
|
||||
.git
|
||||
.gitignore
|
||||
@@ -65,7 +65,6 @@ libaes
|
||||
libappbase
|
||||
libdebugtemp
|
||||
libgpsrelaysentinel
|
||||
libwinboll
|
||||
local.properties-demo
|
||||
mymessagemanager
|
||||
positions
|
||||
@@ -76,9 +75,13 @@ winboll.properties-demo
|
||||
)
|
||||
|
||||
REAL_ITEMS=()
|
||||
# 标准排序ls输出,循环强制过滤 . 和 ..
|
||||
while IFS= read -r line; do
|
||||
[[ $line != "." && $line != ".." ]] && REAL_ITEMS+=("$line")
|
||||
done < <(ls -a)
|
||||
# 跳过虚拟目录 . 和 ..
|
||||
if [[ "$line" != "." && "$line" != ".." ]]; then
|
||||
REAL_ITEMS+=("$line")
|
||||
fi
|
||||
done < <(LC_COLLATE=C ls -a1 --color=none)
|
||||
|
||||
check_diff(){
|
||||
local miss=() extra=()
|
||||
@@ -89,7 +92,17 @@ check_diff(){
|
||||
[[ ! " ${MERGE_OBJECTS_LIST[@]} " =~ " ${i} " ]] && extra+=("$i")
|
||||
done
|
||||
if [[ ${#miss[@]} -gt 0 || ${#extra[@]} -gt 0 ]];then
|
||||
echo "========================================"
|
||||
echo "本地目录结构不一致,终止运行"
|
||||
if [[ ${#miss[@]} -gt 0 ]]; then
|
||||
echo -e "\n缺失条目:"
|
||||
for m in "${miss[@]}"; do echo " $m"; done
|
||||
fi
|
||||
if [[ ${#extra[@]} -gt 0 ]]; then
|
||||
echo -e "\n多余条目:"
|
||||
for e in "${extra[@]}"; do echo " $e"; done
|
||||
fi
|
||||
echo "========================================"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
@@ -98,7 +111,7 @@ check_diff
|
||||
echo -e "#@@@ 按时间获取最新标签合并模块源码 @@@#"
|
||||
|
||||
# 应用型模块
|
||||
MERGE_APP_PROJECT_LIST=(DemoAPP)
|
||||
MERGE_APP_PROJECT_LIST=(WinBoLL)
|
||||
echo -e "---------- 应用型模块 ----------"
|
||||
for name in "${MERGE_APP_PROJECT_LIST[@]}";do
|
||||
low_name=$(echo "$name" | tr 'A-Z' 'a-z')
|
||||
@@ -120,7 +133,7 @@ for name in "${MERGE_APP_PROJECT_LIST[@]}";do
|
||||
done
|
||||
|
||||
# 类库模块
|
||||
MERGE_LIB_PROJECT_LIST=(WinBoLL APPBase AES)
|
||||
MERGE_LIB_PROJECT_LIST=(APPBase AES)
|
||||
echo -e "---------- 类库模块 ----------"
|
||||
for name in "${MERGE_LIB_PROJECT_LIST[@]}";do
|
||||
low_name=$(echo "$name" | tr 'A-Z' 'a-z')
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/bash
|
||||
# ==============================================================================
|
||||
# WinBoLL 应用发布脚本
|
||||
# 功能:检查Git源码状态 → 编译Stage Release包 → 添加WinBoLL标签 → 提交并推送源码
|
||||
# 依赖:build.properties、app_update_description.txt(项目根目录下)
|
||||
# 使用:./script_name.sh <APP_NAME>
|
||||
# 作者:豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
# ==============================================================================
|
||||
|
||||
# ==================== 常量定义 ====================
|
||||
# 脚本退出码
|
||||
EXIT_CODE_SUCCESS=0
|
||||
EXIT_CODE_ERR_NO_APP_NAME=2
|
||||
EXIT_CODE_ERR_WORK_DIR=1
|
||||
EXIT_CODE_ERR_GIT_CHECK=1
|
||||
EXIT_CODE_ERR_ADD_WINBOLL_TAG=1
|
||||
|
||||
# Gradle 任务(正式发布)
|
||||
GRADLE_TASK_PUBLISH="assembleStageRelease"
|
||||
# Gradle 任务(调试用,注释备用)
|
||||
# GRADLE_TASK_DEBUG="assembleBetaDebug"
|
||||
|
||||
# aapt2本地覆盖参数
|
||||
AAPT2_OVERRIDE_ARG="-Pandroid.aapt2FromMavenOverride=/data/data/com.termux/files/usr/bin/aapt2"
|
||||
# 禁用Gradle守护进程
|
||||
GRADLE_NO_DAEMON="--no-daemon"
|
||||
|
||||
# ==================== 函数定义 ====================
|
||||
# 检查Git源码是否已完全提交(无未提交变更)
|
||||
# 返回值:0=已完全提交,1=存在未提交变更
|
||||
function checkGitSources() {
|
||||
# 配置Git安全目录(解决权限问题)
|
||||
git config --global --add safe.directory "$(pwd)"
|
||||
|
||||
# 检查是否有未提交的变更
|
||||
if [[ -n $(git diff --stat) ]]; then
|
||||
echo "[ERROR] Git源码存在未提交变更,请先提交所有修改!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "[INFO] Git源码检查通过:所有变更已提交。"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 询问是否添加GitHub Workflows标签(当前逻辑注释,保留扩展能力)
|
||||
# 返回值:1=用户选择是,0=用户选择否
|
||||
function askAddWorkflowsTag() {
|
||||
read -p "是否添加GitHub Workflows标签?(Y/n) " answer
|
||||
if [[ $answer =~ ^[Yy]$ ]]; then
|
||||
return 1
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# 添加WinBoLL正式标签
|
||||
# 参数:$1=应用名称(项目根目录名)
|
||||
# 返回值:0=标签添加成功,1=标签已存在/添加失败
|
||||
function addWinBoLLTag() {
|
||||
local app_name=$1
|
||||
local build_prop_path="${app_name}/build.properties"
|
||||
|
||||
# 从build.properties中提取publishVersion
|
||||
local publish_version=$(grep -o "publishVersion=.*" "${build_prop_path}" | awk -F '=' '{print $2}')
|
||||
if [[ -z ${publish_version} ]]; then
|
||||
echo "[ERROR] 未从${build_prop_path}中提取到publishVersion配置!"
|
||||
return 1
|
||||
fi
|
||||
echo "[INFO] 从${build_prop_path}读取到publishVersion:${publish_version}"
|
||||
|
||||
# 构造WinBoLL标签(格式:<APP_NAME>-v<publishVersion>)
|
||||
local tag="${app_name}-v${publish_version}"
|
||||
echo "[INFO] 准备添加WinBoLL标签:${tag}"
|
||||
|
||||
# 检查标签是否已存在
|
||||
if [[ "$(git tag -l ${tag})" == "${tag}" ]]; then
|
||||
echo "[ERROR] WinBoLL标签${tag}已存在!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 添加带注释的标签(注释来自app_update_description.txt)
|
||||
git tag -a "${tag}" -F "${app_name}/app_update_description.txt"
|
||||
echo "[INFO] WinBoLL标签${tag}添加成功!"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 添加GitHub Workflows Beta标签(当前逻辑注释,保留扩展能力)
|
||||
# 参数:$1=应用名称(项目根目录名)
|
||||
# 返回值:0=标签添加成功,1=标签已存在/添加失败
|
||||
function addWorkflowsTag() {
|
||||
local app_name=$1
|
||||
local build_prop_path="${app_name}/build.properties"
|
||||
|
||||
# 从build.properties中提取baseBetaVersion
|
||||
local base_beta_version=$(grep -o "baseBetaVersion=.*" "${build_prop_path}" | awk -F '=' '{print $2}')
|
||||
if [[ -z ${base_beta_version} ]]; then
|
||||
echo "[ERROR] 未从${build_prop_path}中提取到baseBetaVersion配置!"
|
||||
return 1
|
||||
fi
|
||||
echo "[INFO] 从${build_prop_path}读取到baseBetaVersion:${base_beta_version}"
|
||||
|
||||
# 构造Workflows标签(格式:<APP_NAME>-v<baseBetaVersion>-beta)
|
||||
local tag="${app_name}-v${base_beta_version}-beta"
|
||||
echo "[INFO] 准备添加Workflows标签:${tag}"
|
||||
|
||||
# 检查标签是否已存在
|
||||
if [[ "$(git tag -l ${tag})" == "${tag}" ]]; then
|
||||
echo "[ERROR] Workflows标签${tag}已存在!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 添加带注释的标签(注释来自app_update_description.txt)
|
||||
git tag -a "${tag}" -F "${app_name}/app_update_description.txt"
|
||||
echo "[INFO] Workflows标签${tag}添加成功!"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ==================== 主流程开始 ====================
|
||||
echo "============================================="
|
||||
echo " WinBoLL 应用发布脚本"
|
||||
echo "============================================="
|
||||
|
||||
# 1. 检查应用名称参数是否指定
|
||||
if [ -z "$1" ]; then
|
||||
echo "[ERROR] 未指定应用名称!使用方式:${0} <APP_NAME>"
|
||||
exit ${EXIT_CODE_ERR_NO_APP_NAME}
|
||||
fi
|
||||
APP_NAME=$1
|
||||
echo "[INFO] 待发布应用名称:${APP_NAME}"
|
||||
|
||||
# 2. 检查并切换到项目根目录(确保build.properties存在)
|
||||
echo "[INFO] 当前工作目录:$(pwd)"
|
||||
if [[ ! -e "${APP_NAME}/build.properties" ]]; then
|
||||
echo "[WARNING] 当前目录不存在${APP_NAME}/build.properties,尝试切换到上级目录..."
|
||||
cd ..
|
||||
echo "[INFO] 切换后工作目录:$(pwd)"
|
||||
fi
|
||||
|
||||
# 验证最终工作目录是否正确
|
||||
if [[ ! -e "${APP_NAME}/build.properties" ]]; then
|
||||
echo "[ERROR] 工作目录错误!${APP_NAME}/build.properties 文件不存在。"
|
||||
exit ${EXIT_CODE_ERR_WORK_DIR}
|
||||
fi
|
||||
echo "[INFO] 工作目录验证通过:${APP_NAME}/build.properties 存在。"
|
||||
|
||||
# 3. 检查Git源码状态
|
||||
echo "---------------------------------------------"
|
||||
echo " 步骤1:检查Git源码状态"
|
||||
echo "---------------------------------------------"
|
||||
checkGitSources
|
||||
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
|
||||
echo "[ERROR] Git源码检查失败,脚本终止!"
|
||||
exit ${EXIT_CODE_ERR_GIT_CHECK}
|
||||
fi
|
||||
|
||||
# 4. 编译Stage Release版本APK(携带aapt2覆盖参数 + --no-daemon)
|
||||
echo "---------------------------------------------"
|
||||
echo " 步骤2:编译Stage Release APK"
|
||||
echo "---------------------------------------------"
|
||||
echo "[INFO] 开始执行Gradle任务:${GRADLE_TASK_PUBLISH}"
|
||||
# 调试用(注释正式任务,启用调试任务)
|
||||
# bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :${APP_NAME}:${GRADLE_TASK_DEBUG}
|
||||
bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :${APP_NAME}:${GRADLE_TASK_PUBLISH}
|
||||
|
||||
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
|
||||
echo "[ERROR] Gradle编译任务失败!"
|
||||
exit 1
|
||||
fi
|
||||
echo "[INFO] Stage Release APK编译成功!"
|
||||
|
||||
# 5. 添加WinBoLL正式标签
|
||||
echo "---------------------------------------------"
|
||||
echo " 步骤3:添加WinBoLL标签"
|
||||
echo "---------------------------------------------"
|
||||
addWinBoLLTag ${APP_NAME}
|
||||
if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
|
||||
echo "[ERROR] WinBoLL标签添加失败,脚本终止!"
|
||||
exit ${EXIT_CODE_ERR_ADD_WINBOLL_TAG}
|
||||
fi
|
||||
|
||||
# 6. (可选)添加GitHub Workflows标签(当前逻辑注释,保留扩展能力)
|
||||
# echo "---------------------------------------------"
|
||||
# echo " 步骤4:添加Workflows标签(可选)"
|
||||
# echo "---------------------------------------------"
|
||||
# echo "是否添加GitHub Workflows Beta标签?(Y/n) "
|
||||
# askAddWorkflowsTag
|
||||
# nAskAddWorkflowsTag=$?
|
||||
# if [[ ${nAskAddWorkflowsTag} -eq 1 ]]; then
|
||||
# addWorkflowsTag ${APP_NAME}
|
||||
# if [[ $? -ne ${EXIT_CODE_SUCCESS} ]]; then
|
||||
# echo "[ERROR] Workflows标签添加失败,脚本终止!"
|
||||
# exit 1
|
||||
# fi
|
||||
# fi
|
||||
|
||||
# 7. 清理更新描述文件
|
||||
echo "---------------------------------------------"
|
||||
echo " 步骤5:清理更新描述文件"
|
||||
echo "---------------------------------------------"
|
||||
echo "" > "${APP_NAME}/app_update_description.txt"
|
||||
echo "[INFO] 已清空${APP_NAME}/app_update_description.txt"
|
||||
|
||||
# 8. 提交并推送源码与标签
|
||||
echo "---------------------------------------------"
|
||||
echo " 步骤6:提交并推送源码"
|
||||
echo "---------------------------------------------"
|
||||
git add .
|
||||
git commit -m "<${APP_NAME}> 开始新的Stage版本开发。"
|
||||
echo "[INFO] 源码提交成功,开始推送..."
|
||||
|
||||
# 推送源码到远程仓库
|
||||
git push origin
|
||||
# 推送标签到远程仓库
|
||||
git push origin --tags
|
||||
|
||||
if [[ $? -eq ${EXIT_CODE_SUCCESS} ]]; then
|
||||
echo "[INFO] 源码与标签推送成功!"
|
||||
else
|
||||
echo "[ERROR] 源码与标签推送失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ==================== 主流程结束 ====================
|
||||
echo "============================================="
|
||||
echo " WinBoLL 应用发布完成!"
|
||||
echo "============================================="
|
||||
exit ${EXIT_CODE_SUCCESS}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
# aapt2本地覆盖参数
|
||||
AAPT2_OVERRIDE_ARG="-Pandroid.aapt2FromMavenOverride=/data/data/com.termux/files/usr/bin/aapt2"
|
||||
# Gradle禁用守护进程参数
|
||||
GRADLE_NO_DAEMON="--no-daemon"
|
||||
|
||||
# 检查是否指定了将要发布的类库名称
|
||||
# 使用 `-z` 命令检查变量是否为空
|
||||
if [ -z "$1" ]; then
|
||||
echo "No Library name specified : $0"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
## 正式发布使用
|
||||
git pull && bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :$1:publishReleasePublicationToWinBoLLReleaseRepository && bash .winboll/bashCommitLibReleaseBuildFlagInfo.sh $1
|
||||
|
||||
## 调试使用
|
||||
#bash gradlew ${AAPT2_OVERRIDE_ARG} ${GRADLE_NO_DAEMON} :$1:publishSnapshotWinBoLLPublicationToWinBoLLSnapshotRepository && bash .winboll/bashCommitLibReleaseBuildFlagInfo.sh $1
|
||||
|
||||
@@ -122,7 +122,6 @@ android {
|
||||
// 如果正在调试,就拷贝到 WinBoLL 备份管理文件夹
|
||||
//
|
||||
if(variant.flavorName == "beta"&&variant.buildType.name == "debug"){
|
||||
//File outBuildBckDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/${variant.buildType.name}")
|
||||
File outBuildBckDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/${variant.buildType.name}")
|
||||
// 创建目标路径目录
|
||||
if(!outBuildBckDir.exists()) {
|
||||
@@ -130,6 +129,7 @@ android {
|
||||
println "Output Folder Created.(WinBoLLStudio) : " + outBuildBckDir.getAbsolutePath()
|
||||
}
|
||||
if(outBuildBckDir.exists()) {
|
||||
def targetApkFile = new File(outBuildBckDir, outputFileName)
|
||||
copy{
|
||||
from file.outputFile
|
||||
into outBuildBckDir
|
||||
@@ -138,6 +138,14 @@ android {
|
||||
}
|
||||
println "Output APK (WinBoLLStudio): " + outBuildBckDir.getAbsolutePath() + "/${outputFileName}"
|
||||
}
|
||||
// ========== 设置文件权限为775 ==========
|
||||
if(targetApkFile.exists()){
|
||||
exec {
|
||||
commandLine 'chmod', '775', targetApkFile.absolutePath
|
||||
}
|
||||
println "Set file permission to 775 : ${targetApkFile.absolutePath}"
|
||||
}
|
||||
|
||||
// 检查编译标志位配置
|
||||
assert (winbollBuildProps['buildCount'] != null)
|
||||
assert (winbollBuildProps['libraryProject'] != null)
|
||||
@@ -160,8 +168,7 @@ android {
|
||||
assert(libraryProjectBuildPropsFile.exists())
|
||||
java.nio.file.Path sourceFilePath = winbollBuildPropsFile.toPath();
|
||||
java.nio.file.Path targetFilePath = libraryProjectBuildPropsFile.toPath();
|
||||
// 使用copyTo()方法复制文件,如果目标文件存在会被覆盖,可选参数可以选择不覆盖
|
||||
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
println "\n\n>>> Library Project build.properties saved.\n\n";
|
||||
}
|
||||
@@ -172,16 +179,12 @@ android {
|
||||
//
|
||||
if(variant.flavorName == "stage"&&variant.buildType.name == "release"){
|
||||
// 发布 APK 文件
|
||||
//
|
||||
// 截取版本号的版本字段为短版本名
|
||||
String szVersionName = "${versionName}"
|
||||
String[] szlistTemp = szVersionName.split("-")
|
||||
String szShortVersionName = szlistTemp[0]
|
||||
//String szCommonTagAPKName = "${rootProject.name}_" + szShortVersionName + ".apk"
|
||||
String szCommonTagAPKName = project.rootDir.name + "_" + szShortVersionName + ".apk"
|
||||
println "CommonTagAPKName is : " + szCommonTagAPKName
|
||||
|
||||
//File outTagDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/tag/")
|
||||
File outTagDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/tag/")
|
||||
// 创建目标路径目录
|
||||
if(!outTagDir.exists()) {
|
||||
@@ -192,12 +195,10 @@ android {
|
||||
if(outTagDir.exists()) {
|
||||
File targetAPK = new File(outTagDir, "${szCommonTagAPKName}")
|
||||
if(targetAPK.exists()) {
|
||||
// 标签版本APK文件已经存在,构建拷贝任务停止
|
||||
assert (!targetAPK.exists())
|
||||
// 可选择删除并继续输出APK文件
|
||||
//delete targetAPK
|
||||
}
|
||||
// 复制一个备份
|
||||
// 复制完整版APK
|
||||
def fullApkFile = new File(outTagDir, outputFileName)
|
||||
copy{
|
||||
from file.outputFile
|
||||
into outTagDir
|
||||
@@ -206,7 +207,16 @@ android {
|
||||
}
|
||||
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${outputFileName}"
|
||||
}
|
||||
// 复制一个并重命名为短版本名
|
||||
// 设置权限775。
|
||||
if(fullApkFile.exists()){
|
||||
exec {
|
||||
commandLine 'chmod', '775', fullApkFile.absolutePath
|
||||
}
|
||||
println "Set file permission to 775 : ${fullApkFile.absolutePath}"
|
||||
}
|
||||
|
||||
// 复制短版本名APK
|
||||
def shortApkFile = new File(outTagDir, szCommonTagAPKName)
|
||||
copy{
|
||||
from file.outputFile
|
||||
into outTagDir
|
||||
@@ -215,6 +225,14 @@ android {
|
||||
}
|
||||
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${szCommonTagAPKName}"
|
||||
}
|
||||
// 设置权限775。
|
||||
if(shortApkFile.exists()){
|
||||
exec {
|
||||
commandLine 'chmod', '775', shortApkFile.absolutePath
|
||||
}
|
||||
println "Set file permission to 775 : ${shortApkFile.absolutePath}"
|
||||
}
|
||||
|
||||
// 检查编译标志位配置
|
||||
assert (winbollBuildProps['stageCount'] != null)
|
||||
assert (winbollBuildProps['publishVersion'] != null)
|
||||
@@ -239,14 +257,11 @@ android {
|
||||
fos.close();
|
||||
|
||||
if(winbollBuildProps['libraryProject'] != "") {
|
||||
// 如果应用 build.properties 文件设置了类库模块项目文件名
|
||||
// 就拷贝一份新的编译标志配置到类库项目文件夹
|
||||
File libraryProjectBuildPropsFile = new File("$RootProjectDir/" + winbollBuildProps['libraryProject'] + "/build.properties")
|
||||
assert(winbollBuildPropsFile.exists())
|
||||
assert(libraryProjectBuildPropsFile.exists())
|
||||
java.nio.file.Path sourceFilePath = winbollBuildPropsFile.toPath();
|
||||
java.nio.file.Path targetFilePath = libraryProjectBuildPropsFile.toPath();
|
||||
// 使用copyTo()方法复制文件,如果目标文件存在会被覆盖,可选参数可以选择不覆盖
|
||||
java.nio.file.Files.copy(sourceFilePath, targetFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
@@ -263,17 +278,12 @@ android {
|
||||
// 如果正在调试发布版,就只生成和输出APK文件,不处理 Git 仓库提交与更新问题。
|
||||
//
|
||||
if(variant.flavorName == "stage"&&variant.buildType.name == "debug"){
|
||||
// 发布 APK 文件
|
||||
//
|
||||
// 截取版本号的版本字段为短版本名
|
||||
String szVersionName = "${versionName}"
|
||||
String[] szlistTemp = szVersionName.split("-")
|
||||
String szShortVersionName = szlistTemp[0]
|
||||
//String szCommonTagAPKName = "${rootProject.name}_" + szShortVersionName + ".apk"
|
||||
String szCommonTagAPKName = project.rootDir.name + "_" + szShortVersionName + ".apk"
|
||||
println "CommonTagAPKName is : " + szCommonTagAPKName
|
||||
|
||||
//File outTagDir = new File(fWinBoLLStudioDir, "/${rootProject.name}/tag/")
|
||||
File outTagDir = new File(fWinBoLLStudioDir, "/" + project.rootDir.name + "/${variant.buildType.name}/")
|
||||
// 创建目标路径目录
|
||||
if(!outTagDir.exists()) {
|
||||
@@ -284,13 +294,11 @@ android {
|
||||
if(outTagDir.exists()) {
|
||||
File targetAPK = new File(outTagDir, "${szCommonTagAPKName}")
|
||||
if(targetAPK.exists()) {
|
||||
// 标签版本APK文件已经存在,构建拷贝任务停止
|
||||
println '如果是在调试 Stage 版应用包构建,请删除(注:在debug目录)现有的 Stage 应用包('+targetAPK.getAbsolutePath()+')。再编译一次。'
|
||||
assert (!targetAPK.exists())
|
||||
// 可选择删除并继续输出APK文件
|
||||
//delete targetAPK
|
||||
}
|
||||
// 复制一个备份
|
||||
// 复制完整版APK
|
||||
def debugFullApk = new File(outTagDir, outputFileName)
|
||||
copy{
|
||||
from file.outputFile
|
||||
into outTagDir
|
||||
@@ -299,7 +307,16 @@ android {
|
||||
}
|
||||
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${outputFileName}"
|
||||
}
|
||||
// 复制一个并重命名为短版本名
|
||||
// 权限设为775。
|
||||
if(debugFullApk.exists()){
|
||||
exec {
|
||||
commandLine 'chmod', '775', debugFullApk.absolutePath
|
||||
}
|
||||
println "Set file permission to 775 : ${debugFullApk.absolutePath}"
|
||||
}
|
||||
|
||||
// 复制短版本名APK
|
||||
def debugShortApk = new File(outTagDir, szCommonTagAPKName)
|
||||
copy{
|
||||
from file.outputFile
|
||||
into outTagDir
|
||||
@@ -308,8 +325,13 @@ android {
|
||||
}
|
||||
println "Output APK (Tags): "+ outTagDir.getAbsolutePath() + "/${szCommonTagAPKName}"
|
||||
}
|
||||
|
||||
//不保存编译标志配置
|
||||
// 权限设为775
|
||||
if(debugShortApk.exists()){
|
||||
exec {
|
||||
commandLine 'chmod', '775', debugShortApk.absolutePath
|
||||
}
|
||||
println "Set file permission to 775 : ${debugShortApk.absolutePath}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,6 +350,13 @@ android {
|
||||
}
|
||||
println "Output APK (Common): " + outCommonDir.getAbsolutePath() + "/${commandAPKName}"
|
||||
}
|
||||
// 额外输出文件设置775权限
|
||||
if(apkFile.exists()){
|
||||
exec {
|
||||
commandLine 'chmod', '775', apkFile.absolutePath
|
||||
}
|
||||
println "Set file permission to 775 : ${apkFile.absolutePath}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
15.20
|
||||
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,97 @@
|
||||
# WinBoLL 源码 LICENSE-Private-Demo 规范说明书
|
||||
|
||||
# LICENSE-Private-Demo
|
||||
|
||||
# WinBoLL 源码公共转私有继承开发规范守则
|
||||
|
||||
## 核心声明
|
||||
|
||||
本文档**唯一核心设计目的**:通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
|
||||
|
||||
## 一、文件宗旨与风险防控说明
|
||||
|
||||
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
|
||||
|
||||
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
|
||||
|
||||
2. **重点防控人为操作导致的私有分支代码违规合并、回合、推送至公共 ****`winboll`**** 主流分支**,从流程上封堵合并风险
|
||||
|
||||
3. 明确所有开发提交者的操作责任,违规合并公共分支的行为由操作人承担全部代码泄露、合规风险
|
||||
|
||||
4. 规范私有分支初始化全流程,保证私有分支仅可单向同步公共分支更新,禁止任何反向代码流入公共分支
|
||||
|
||||
## 二、公私分支授权标识文件定义(风控核心依据)
|
||||
|
||||
### 1. 公共开源分支唯一标识
|
||||
|
||||
**文件名:LICENSE**
|
||||
|
||||
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
|
||||
|
||||
- 标识当前分支为**开源公开可贡献分支**,遵循原开源授权协议
|
||||
|
||||
- **严禁私有分支内保留、恢复此文件**,出现即判定分支属性异常
|
||||
|
||||
### 2. 私有开发分支唯一标识
|
||||
|
||||
**文件名:LICENSE-Private**
|
||||
|
||||
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
|
||||
|
||||
- 标识当前分支为**私有闭源分支**,代码仅限内部使用,禁止公开、禁止对外贡献
|
||||
|
||||
- 为本分支私有属性的法定判定依据,也是禁止合并至公共分支的核心标记
|
||||
|
||||
## 三、分支管理与合并风控规则(强制遵守)
|
||||
|
||||
1. **公共主流分支**:固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
|
||||
|
||||
2. **私有开发分支**:统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
|
||||
|
||||
3. **核心合并风控铁则**
|
||||
|
||||
- 私有分支 → 公共分支:**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
|
||||
|
||||
- 公共分支 → 私有分支:允许正常拉取、同步上游更新,不影响私有开发迭代
|
||||
|
||||
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
|
||||
|
||||
## 四、公共转私有标准化操作步骤(锁死合并风险)
|
||||
|
||||
请严格按顺序执行,每一步均为风控必要环节,不可跳过、不可修改顺序。
|
||||
|
||||
1. 基于公共主流分支 `winboll`,新建私有开发分支,严格使用 `private-demo-*` 命名,从名称上明确分支私有属性,避免人为混淆。
|
||||
|
||||
2. 本地仓库切换至新建私有分支,确认当前分支名称、检出来源无误。
|
||||
|
||||
3. **永久删除项目根目录公共授权文件 ****`LICENSE`**,彻底移除公共分支标识,断绝误合并的标识漏洞。
|
||||
|
||||
4. 将本规范文件 `LICENSE-Private-Demo` 复制并重命名为 `LICENSE-Private`,作为私有分支生效授权文件。
|
||||
|
||||
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
|
||||
|
||||
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
|
||||
>
|
||||
>
|
||||
|
||||
6. 提交完成后,本分支正式转为私有开发状态,后续所有代码提交、分支合并、版本迭代,均严禁指向公共 `winboll` 分支。
|
||||
|
||||
## 五、人为操作责任界定(核心补充条款)
|
||||
|
||||
1. 本分支所有开发者、代码提交者、分支合并操作者,均视为**完全知晓本分支的私有属性与合并禁令**,自愿遵守本规范全部约束。
|
||||
|
||||
2. **无论故意或过失,凡是人为执行私有分支向公共 ****`winboll`**** 分支的合并、推送、PR 提交、代码回合操作,全部责任由执行操作的本人独立承担**,项目方不承担任何因人为违规操作导致的代码泄露、开源合规、版本污染风险。
|
||||
|
||||
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
|
||||
|
||||
4. 分支属性校验以根目录授权文件为唯一标准:只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
|
||||
|
||||
## 六、分支状态校验与异常处理
|
||||
|
||||
- 合规公共分支:仅存在 `LICENSE`,无 `LICENSE-Private`
|
||||
|
||||
- 合规私有分支:仅存在 `LICENSE-Private`,无 `LICENSE`
|
||||
|
||||
- 异常状态:两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
|
||||
|
||||
> (注:文档部分内容可能由 AI 生成)
|
||||
+14
-14
@@ -6,11 +6,11 @@
|
||||
|
||||
## 核心声明
|
||||
|
||||
本文档**唯一核心设计目的**: 通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
|
||||
本文档**唯一核心设计目的**:通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
|
||||
|
||||
## 一、文件宗旨与风险防控说明
|
||||
|
||||
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
|
||||
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
|
||||
|
||||
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
### 1. 公共开源分支唯一标识
|
||||
|
||||
**文件名: LICENSE**
|
||||
**文件名:LICENSE**
|
||||
|
||||
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
### 2. 私有开发分支唯一标识
|
||||
|
||||
**文件名: LICENSE-Private**
|
||||
**文件名:LICENSE-Private**
|
||||
|
||||
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
|
||||
|
||||
@@ -44,15 +44,15 @@
|
||||
|
||||
## 三、分支管理与合并风控规则(强制遵守)
|
||||
|
||||
1. **公共主流分支**: 固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
|
||||
1. **公共主流分支**:固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
|
||||
|
||||
2. **私有开发分支**: 统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
|
||||
2. **私有开发分支**:统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
|
||||
|
||||
3. **核心合并风控铁则**
|
||||
|
||||
- 私有分支 → 公共分支: **永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
|
||||
- 私有分支 → 公共分支:**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
|
||||
|
||||
- 公共分支 → 私有分支: 允许正常拉取、同步上游更新,不影响私有开发迭代
|
||||
- 公共分支 → 私有分支:允许正常拉取、同步上游更新,不影响私有开发迭代
|
||||
|
||||
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
4. 将本规范文件 `LICENSE-Private-Demo` 复制并重命名为 `LICENSE-Private`,作为私有分支生效授权文件。
|
||||
|
||||
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
|
||||
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
|
||||
|
||||
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
|
||||
>
|
||||
@@ -84,14 +84,14 @@
|
||||
|
||||
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
|
||||
|
||||
4. 分支属性校验以根目录授权文件为唯一标准: 只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
|
||||
4. 分支属性校验以根目录授权文件为唯一标准:只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
|
||||
|
||||
## 六、分支状态校验与异常处理
|
||||
|
||||
- 合规公共分支: 仅存在 `LICENSE`,无 `LICENSE-Private`
|
||||
- 合规公共分支:仅存在 `LICENSE`,无 `LICENSE-Private`
|
||||
|
||||
- 合规私有分支: 仅存在 `LICENSE-Private`,无 `LICENSE`
|
||||
- 合规私有分支:仅存在 `LICENSE-Private`,无 `LICENSE`
|
||||
|
||||
- 异常状态: 两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
|
||||
- 异常状态:两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
|
||||
|
||||
> (注: 文档部分内容可能由 AI 生成)
|
||||
> (注:文档部分内容可能由 AI 生成)
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
|
||||
## 核心声明
|
||||
|
||||
本文档**唯一核心设计目的**: 通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
|
||||
本文档**唯一核心设计目的**:通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
|
||||
|
||||
## 一、文件宗旨与风险防控说明
|
||||
|
||||
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
|
||||
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
|
||||
|
||||
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
### 1. 公共开源分支唯一标识
|
||||
|
||||
**文件名: LICENSE**
|
||||
**文件名:LICENSE**
|
||||
|
||||
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
### 2. 私有开发分支唯一标识
|
||||
|
||||
**文件名: LICENSE-Private**
|
||||
**文件名:LICENSE-Private**
|
||||
|
||||
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
|
||||
|
||||
@@ -44,15 +44,15 @@
|
||||
|
||||
## 三、分支管理与合并风控规则(强制遵守)
|
||||
|
||||
1. **公共主流分支**: 固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
|
||||
1. **公共主流分支**:固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
|
||||
|
||||
2. **私有开发分支**: 统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
|
||||
2. **私有开发分支**:统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
|
||||
|
||||
3. **核心合并风控铁则**
|
||||
|
||||
- 私有分支 → 公共分支: **永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
|
||||
- 私有分支 → 公共分支:**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
|
||||
|
||||
- 公共分支 → 私有分支: 允许正常拉取、同步上游更新,不影响私有开发迭代
|
||||
- 公共分支 → 私有分支:允许正常拉取、同步上游更新,不影响私有开发迭代
|
||||
|
||||
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
4. 将本规范文件 `LICENSE-Private-Demo` 复制并重命名为 `LICENSE-Private`,作为私有分支生效授权文件。
|
||||
|
||||
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
|
||||
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
|
||||
|
||||
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
|
||||
>
|
||||
@@ -84,14 +84,14 @@
|
||||
|
||||
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
|
||||
|
||||
4. 分支属性校验以根目录授权文件为唯一标准: 只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
|
||||
4. 分支属性校验以根目录授权文件为唯一标准:只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
|
||||
|
||||
## 六、分支状态校验与异常处理
|
||||
|
||||
- 合规公共分支: 仅存在 `LICENSE`,无 `LICENSE-Private`
|
||||
- 合规公共分支:仅存在 `LICENSE`,无 `LICENSE-Private`
|
||||
|
||||
- 合规私有分支: 仅存在 `LICENSE-Private`,无 `LICENSE`
|
||||
- 合规私有分支:仅存在 `LICENSE-Private`,无 `LICENSE`
|
||||
|
||||
- 异常状态: 两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
|
||||
- 异常状态:两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
|
||||
|
||||
> (注: 文档部分内容可能由 AI 生成)
|
||||
> (注:文档部分内容可能由 AI 生成)
|
||||
|
||||
@@ -39,10 +39,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// 米盟 SDK
|
||||
packagingOptions {
|
||||
doNotStrip "*/*/libmimo_1011.so"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sun May 24 10:48:48 HKT 2026
|
||||
stageCount=10
|
||||
#Sat Jul 11 20:07:00 HKT 2026
|
||||
stageCount=19
|
||||
libraryProject=libaes
|
||||
baseVersion=15.20
|
||||
publishVersion=15.20.9
|
||||
publishVersion=15.20.18
|
||||
buildCount=0
|
||||
baseBetaVersion=15.20.10
|
||||
baseBetaVersion=15.20.19
|
||||
|
||||
@@ -65,7 +65,7 @@ public class AboutActivity extends BaseWinBoLLActivity {
|
||||
appInfo.setAppName(getString(R.string.app_name));
|
||||
appInfo.setAppIcon(R.drawable.ic_winboll);
|
||||
appInfo.setAppDescription(getString(R.string.app_description));
|
||||
appInfo.setAppGitName("AES");
|
||||
appInfo.setAppGitName("WinBoLL");
|
||||
appInfo.setAppGitOwner("Studio");
|
||||
appInfo.setAppGitAPPBranch(branchName);
|
||||
appInfo.setAppGitAPPSubProjectFolder(branchName);
|
||||
|
||||
@@ -5,10 +5,14 @@ package cc.winboll.studio.aes;
|
||||
* @Date 2024/06/13 19:03:58
|
||||
* @Describe AES应用类
|
||||
*/
|
||||
import android.view.Gravity;
|
||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.CrashActivity;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
|
||||
|
||||
public class App extends GlobalApplication {
|
||||
@@ -17,11 +21,25 @@ public class App extends GlobalApplication {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
WinBoLLActivityManager.init(this);
|
||||
try {
|
||||
super.onCreate();
|
||||
ToastUtils.init(this);
|
||||
WinBoLLActivityManager.init(this);
|
||||
AESThemeUtil.init(null);
|
||||
} catch (Throwable e) {
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
e.printStackTrace(pw);
|
||||
pw.close();
|
||||
String stackTraceStr = sw.toString();
|
||||
CrashHandleNotifyUtils.handleUncaughtException(
|
||||
this,
|
||||
getPackageName(),
|
||||
stackTraceStr,
|
||||
CrashActivity.class
|
||||
);
|
||||
}
|
||||
|
||||
// 初始化 Toast 框架
|
||||
ToastUtils.init(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -84,11 +84,12 @@ public class MainActivity extends DrawerFragmentActivity {
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||
// if(App.isDebugging()) {
|
||||
// getMenuInflater().inflate(cc.winboll.studio.libaes.R.menu.toolbar_studio_debug, menu);
|
||||
// }
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
package cc.winboll.studio.aes;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import cc.winboll.studio.libaes.views.ADsControlView;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
@@ -20,7 +16,7 @@ public class SettingsActivity extends Activity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
ADsControlView adsControlView = (ADsControlView) findViewById(R.id.ads_control_view);
|
||||
//ADsControlView adsControlView = (ADsControlView) findViewById(R.id.ads_control_view);
|
||||
|
||||
// adsControlView.setOnAdsModeSelectedListener(new ADsControlView.OnAdsModeSelectedListener() {
|
||||
// @Override
|
||||
|
||||
@@ -6,12 +6,5 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" android:background="@drawable/bg_container_border">
|
||||
|
||||
<cc.winboll.studio.libaes.views.ADsControlView
|
||||
android:id="@+id/ads_control_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_frame"
|
||||
android:padding="10dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</style>
|
||||
|
||||
<style name="MyDebugActivityTheme" parent="Theme.AppCompat.NoActionBar">
|
||||
<item name="android:statusBarColor">@color/toolbarBackgroundColor</item>
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryDark</item>
|
||||
<item name="colorTittle">@color/mainWindowTextColor</item>
|
||||
<item name="colorTittleBackgound">@color/toolbarBackgroundColor</item>
|
||||
<item name="colorText">@color/debugTextColor</item>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</style>
|
||||
|
||||
<style name="MyDebugActivityTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:statusBarColor">@color/toolbarBackgroundColor</item>
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryDark</item>
|
||||
<item name="colorTittle">@color/mainWindowTextColor</item>
|
||||
<item name="colorTittleBackgound">@color/toolbarBackgroundColor</item>
|
||||
<item name="colorText">@color/debugTextColor</item>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Wed May 20 16:12:13 HKT 2026
|
||||
stageCount=21
|
||||
#Sun Jun 28 20:55:33 HKT 2026
|
||||
stageCount=35
|
||||
libraryProject=libappbase
|
||||
baseVersion=15.20
|
||||
publishVersion=15.20.20
|
||||
publishVersion=15.20.34
|
||||
buildCount=0
|
||||
baseBetaVersion=15.20.21
|
||||
baseBetaVersion=15.20.35
|
||||
|
||||
@@ -37,7 +37,7 @@ public class AboutActivity extends Activity {
|
||||
appInfo.setAppName("APPBase");
|
||||
appInfo.setAppIcon(R.drawable.ic_winboll);
|
||||
appInfo.setAppDescription(getString(R.string.app_description));
|
||||
appInfo.setAppGitName("APPBase");
|
||||
appInfo.setAppGitName("WinBoLL");
|
||||
appInfo.setAppGitOwner("Studio");
|
||||
appInfo.setAppGitAPPBranch(branchName);
|
||||
appInfo.setAppGitAPPSubProjectFolder(branchName);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package cc.winboll.studio.appbase;
|
||||
|
||||
import cc.winboll.studio.libappbase.CrashActivity;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.BuildConfig;
|
||||
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
@@ -21,10 +24,24 @@ public class App extends GlobalApplication {
|
||||
*/
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
|
||||
ToastUtils.init(getApplicationContext());
|
||||
try {
|
||||
super.onCreate();
|
||||
|
||||
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
|
||||
ToastUtils.init(getApplicationContext());
|
||||
} catch (Throwable e) {
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
e.printStackTrace(pw);
|
||||
pw.close();
|
||||
String stackTraceStr = sw.toString();
|
||||
CrashHandleNotifyUtils.handleUncaughtException(
|
||||
this,
|
||||
getPackageName(),
|
||||
stackTraceStr,
|
||||
CrashActivity.class
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Gallery
|
||||
|
||||
#### 介绍
|
||||
云宝相册应用
|
||||
|
||||
#### 软件架构
|
||||
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
||||
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
|
||||
|
||||
|
||||
#### Gradle 编译说明
|
||||
调试版编译命令 :gradle assembleBetaDebug
|
||||
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh gallery
|
||||
|
||||
#### 使用说明
|
||||
|
||||
#### 参与贡献
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 新建 Feat_xxx 分支
|
||||
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
|
||||
4. 新建 Pull Request
|
||||
|
||||
|
||||
#### 特技
|
||||
|
||||
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
|
||||
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
|
||||
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
|
||||
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
|
||||
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
|
||||
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
||||
|
||||
#### 参考文档
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply from: '../.winboll/winboll_app_build.gradle'
|
||||
apply from: '../.winboll/winboll_lint_build.gradle'
|
||||
|
||||
def genVersionName(def versionName){
|
||||
// 检查编译标志位配置
|
||||
assert (winbollBuildProps['stageCount'] != null)
|
||||
assert (winbollBuildProps['baseVersion'] != null)
|
||||
// 保存基础版本号
|
||||
winbollBuildProps.setProperty("baseVersion", "${versionName}");
|
||||
//保存编译标志配置
|
||||
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
|
||||
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
|
||||
fos.close();
|
||||
|
||||
// 返回编译版本号
|
||||
return "${versionName}." + winbollBuildProps['stageCount']
|
||||
}
|
||||
|
||||
android {
|
||||
// 适配MIUI12
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "cc.winboll.studio.gallery"
|
||||
minSdkVersion 26
|
||||
// 适配MIUI12
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
// versionName 更新后需要手动设置
|
||||
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||
versionName "15.0"
|
||||
if(true) {
|
||||
versionName = genVersionName("${versionName}")
|
||||
}
|
||||
}
|
||||
|
||||
// 米盟 SDK
|
||||
packagingOptions {
|
||||
doNotStrip "*/*/libmimo_1011.so"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDirs = ['libs'] // 若SO库放在libs目录下
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
api 'com.google.code.gson:gson:2.10.1'
|
||||
|
||||
// 下拉控件
|
||||
api 'com.baoyz.pullrefreshlayout:library:1.2.0'
|
||||
|
||||
// SSH
|
||||
api 'com.jcraft:jsch:0.1.55'
|
||||
// Html 解析
|
||||
api 'org.jsoup:jsoup:1.13.1'
|
||||
// 二维码类库
|
||||
api 'com.google.zxing:core:3.4.1'
|
||||
api 'com.journeyapps:zxing-android-embedded:3.6.0'
|
||||
// 应用介绍页类库
|
||||
api 'io.github.medyo:android-about-page:2.0.0'
|
||||
// 网络连接类库
|
||||
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
||||
// OkHttp网络请求
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
|
||||
// FastJSON解析
|
||||
implementation 'com.alibaba:fastjson:1.2.76'
|
||||
|
||||
// AndroidX 类库
|
||||
/*api 'androidx.appcompat:appcompat:1.1.0'
|
||||
//api 'com.google.android.material:material:1.4.0'
|
||||
//api 'androidx.viewpager:viewpager:1.0.0'
|
||||
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
|
||||
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
|
||||
//api 'androidx.fragment:fragment:1.1.0'*/
|
||||
|
||||
|
||||
// 米盟
|
||||
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
|
||||
//注意:以下5个库必须要引入
|
||||
//implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
api 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
api 'com.google.code.gson:gson:2.8.5'
|
||||
api 'com.github.bumptech.glide:glide:4.9.0'
|
||||
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
||||
|
||||
implementation "androidx.annotation:annotation:1.3.0"
|
||||
implementation "androidx.core:core:1.6.0"
|
||||
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
||||
implementation "androidx.preference:preference:1.1.1"
|
||||
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||
implementation "com.google.android.material:material:1.4.0"
|
||||
implementation "com.google.guava:guava:24.1-jre"
|
||||
/*
|
||||
implementation "io.noties.markwon:core:$markwonVersion"
|
||||
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
|
||||
implementation "io.noties.markwon:linkify:$markwonVersion"
|
||||
implementation "io.noties.markwon:recycler:$markwonVersion"
|
||||
*/
|
||||
/*implementation 'com.termux:terminal-emulator:0.118.0'
|
||||
implementation 'com.termux:terminal-view:0.118.0'
|
||||
implementation 'com.termux:termux-shared:0.118.0'
|
||||
*/
|
||||
// WinBoLL库 nexus.winboll.cc 地址
|
||||
api 'cc.winboll.studio:libaes:15.20.21'
|
||||
api 'cc.winboll.studio:libappbase:15.20.34'
|
||||
|
||||
// WinBoLL备用库 jitpack.io 地址
|
||||
//api 'com.github.ZhanGSKen:AES:aes-v15.15.7'
|
||||
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4'
|
||||
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Mon Jul 20 19:20:48 HKT 2026
|
||||
stageCount=19
|
||||
libraryProject=
|
||||
baseVersion=15.0
|
||||
publishVersion=15.0.18
|
||||
buildCount=0
|
||||
baseBetaVersion=15.0.19
|
||||
Vendored
+137
@@ -0,0 +1,137 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# ============================== 基础通用规则 ==============================
|
||||
# 保留系统组件
|
||||
-keep public class * extends android.app.Activity
|
||||
-keep public class * extends android.app.Service
|
||||
-keep public class * extends android.content.BroadcastReceiver
|
||||
-keep public class * extends android.content.ContentProvider
|
||||
-keep public class * extends android.app.backup.BackupAgentHelper
|
||||
-keep public class * extends android.preference.Preference
|
||||
|
||||
# 保留 WinBoLL 核心包及子类(合并简化规则)
|
||||
-keep class cc.winboll.studio.** { *; }
|
||||
-keepclassmembers class cc.winboll.studio.** { *; }
|
||||
|
||||
# 保留所有类中的 public static final String TAG 字段(便于日志定位)
|
||||
-keepclassmembers class * {
|
||||
public static final java.lang.String TAG;
|
||||
}
|
||||
|
||||
# 保留序列化类(避免Parcelable/Gson解析异常)
|
||||
-keep class * implements android.os.Parcelable {
|
||||
public static final android.os.Parcelable$Creator *;
|
||||
}
|
||||
-keepclassmembers class * implements java.io.Serializable {
|
||||
static final long serialVersionUID;
|
||||
private static final java.io.ObjectStreamField[] serialPersistentFields;
|
||||
private void writeObject(java.io.ObjectOutputStream);
|
||||
private void readObject(java.io.ObjectInputStream);
|
||||
java.lang.Object writeReplace();
|
||||
java.lang.Object readResolve();
|
||||
}
|
||||
|
||||
# 保留 R 文件(避免资源ID混淆)
|
||||
-keepclassmembers class **.R$* {
|
||||
public static <fields>;
|
||||
}
|
||||
|
||||
# 保留 native 方法(避免JNI调用失败)
|
||||
-keepclasseswithmembernames class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
# 保留注解和泛型(避免反射/序列化异常)
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes Signature
|
||||
|
||||
# 屏蔽 Java 8+ 警告(适配 Java 7 语法)
|
||||
-dontwarn java.lang.invoke.*
|
||||
-dontwarn android.support.v8.renderscript.*
|
||||
-dontwarn java.util.function.**
|
||||
|
||||
# ============================== 第三方框架专项规则 ==============================
|
||||
# OkHttp 4.4.1(米盟广告请求依赖,完善Lambda兼容)
|
||||
-keep class okhttp3.** { *; }
|
||||
-keep interface okhttp3.** { *; }
|
||||
-keep class okhttp3.internal.** { *; }
|
||||
-keep class okio.** { *; }
|
||||
-dontwarn okhttp3.internal.platform.**
|
||||
-dontwarn okio.**
|
||||
|
||||
# Glide 4.9.0(米盟广告图片加载依赖)
|
||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||
-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType {
|
||||
**[] $VALUES;
|
||||
public *;
|
||||
}
|
||||
-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule {
|
||||
<init>();
|
||||
}
|
||||
-dontwarn com.bumptech.glide.**
|
||||
|
||||
# Gson 2.8.5(米盟广告数据序列化依赖)
|
||||
-keep class com.google.gson.** { *; }
|
||||
-keep interface com.google.gson.** { *; }
|
||||
-keepclassmembers class * {
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
}
|
||||
|
||||
# 米盟 SDK(核心广告组件,完整保留避免加载失败)
|
||||
-keep class com.miui.zeus.** { *; }
|
||||
-keep interface com.miui.zeus.** { *; }
|
||||
# 保留米盟日志字段(便于广告加载失败排查)
|
||||
-keepclassmembers class com.miui.zeus.mimo.sdk.** {
|
||||
public static final java.lang.String TAG;
|
||||
}
|
||||
|
||||
# RecyclerView 1.0.0(米盟广告布局渲染依赖)
|
||||
-keep class androidx.recyclerview.** { *; }
|
||||
-keep interface androidx.recyclerview.** { *; }
|
||||
-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter {
|
||||
public *;
|
||||
}
|
||||
|
||||
# 其他第三方框架(按引入依赖保留,无则可删除)
|
||||
# XXPermissions 18.63
|
||||
-keep class com.hjq.permissions.** { *; }
|
||||
-keep interface com.hjq.permissions.** { *; }
|
||||
|
||||
# ZXing 二维码(核心解析组件)
|
||||
-keep class com.google.zxing.** { *; }
|
||||
-keep class com.journeyapps.zxing.** { *; }
|
||||
|
||||
# Jsoup HTML解析
|
||||
-keep class org.jsoup.** { *; }
|
||||
|
||||
# Pinyin4j 拼音搜索
|
||||
-keep class net.sourceforge.pinyin4j.** { *; }
|
||||
|
||||
# JSch SSH组件
|
||||
-keep class com.jcraft.jsch.** { *; }
|
||||
|
||||
# AndroidX 基础组件
|
||||
-keep class androidx.appcompat.** { *; }
|
||||
-keep interface androidx.appcompat.** { *; }
|
||||
|
||||
# ============================== 优化与调试配置 ==============================
|
||||
# 优化级别(平衡混淆效果与性能)
|
||||
-optimizationpasses 5
|
||||
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
|
||||
|
||||
# 调试辅助(保留行号便于崩溃定位)
|
||||
-verbose
|
||||
-dontpreverify
|
||||
-dontusemixedcaseclassnames
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools" >
|
||||
|
||||
<application>
|
||||
|
||||
<!-- Put flavor specific code here -->
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Put flavor specific strings here -->
|
||||
<string name="app_name">Gallery+</string>
|
||||
</resources>
|
||||
@@ -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/GalleryTheme"
|
||||
android:resizeableActivity="true"
|
||||
android:name=".App"
|
||||
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("Gallery");
|
||||
appInfo.setAppGitOwner("ZhanGSKen");
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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,243 @@
|
||||
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.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.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
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 = albums;
|
||||
LogUtils.d(TAG, "setData: " + albums.size() + " albums");
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
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()) {
|
||||
java.util.LinkedHashMap<String, Integer> pinnedPathMap = pinnedDbHelper.getPinnedPathsWithSortOrder();
|
||||
Set<String> pinnedPaths = pinnedPathMap.keySet();
|
||||
ArrayList<Album> pinned = new ArrayList<Album>();
|
||||
ArrayList<Album> unpinned = new ArrayList<Album>();
|
||||
|
||||
// 按照排序号顺序添加置顶相册
|
||||
for (String pinnedPath : pinnedPathMap.keySet()) {
|
||||
for (Album album : albums) {
|
||||
if (album.getPath().equals(pinnedPath)) {
|
||||
pinned.add(album);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加非置顶相册
|
||||
for (Album album : albums) {
|
||||
if (!pinnedPaths.contains(album.getPath())) {
|
||||
unpinned.add(album);
|
||||
}
|
||||
}
|
||||
|
||||
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 final 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,52 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import cc.winboll.studio.gallery.utils.BackgroundUtils;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.CrashActivity;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/07/19
|
||||
* @Describe 应用全局入口类(继承 GlobalWinBoLLApplication)
|
||||
* 负责应用初始化、全局资源管理与生命周期回调处理,是整个应用的核心入口
|
||||
*/
|
||||
public class App extends GlobalApplication {
|
||||
|
||||
public static final String TAG = "App";
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
try {
|
||||
super.onCreate();
|
||||
|
||||
WinBoLLActivityManager.init(this);
|
||||
|
||||
BackgroundUtils.initFromPreferences(this);
|
||||
|
||||
ToastUtils.init(getApplicationContext());
|
||||
} catch (Throwable e) {
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
e.printStackTrace(pw);
|
||||
pw.close();
|
||||
String stackTraceStr = sw.toString();
|
||||
CrashHandleNotifyUtils.handleUncaughtException(
|
||||
this,
|
||||
getPackageName(),
|
||||
stackTraceStr,
|
||||
CrashActivity.class
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
ToastUtils.release();
|
||||
}
|
||||
}
|
||||
@@ -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,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 final 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.os.Handler;
|
||||
import android.os.Looper;
|
||||
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.App;
|
||||
import cc.winboll.studio.gallery.utils.BackgroundUtils;
|
||||
import cc.winboll.studio.libappbase.LogActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
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;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
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;
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
@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() {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final ArrayList<Album> albums = loadAlbumsInBackground();
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (albums.isEmpty()) {
|
||||
Toast.makeText(MainActivity.this, R.string.no_images_found, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
adapter.setData(albums);
|
||||
LogUtils.d(TAG, "Loaded " + albums.size() + " albums");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ArrayList<Album> loadAlbumsInBackground() {
|
||||
LogUtils.d(TAG, "loadAlbumsInBackground");
|
||||
String folderPath = prefs.getFolderPath();
|
||||
File baseFolder = new File(folderPath);
|
||||
|
||||
if (!baseFolder.exists() || !baseFolder.isDirectory()) {
|
||||
folderPath = Preferences.getDefaultPath();
|
||||
baseFolder = new File(folderPath);
|
||||
if (!baseFolder.exists()) {
|
||||
folderPath = Environment.getExternalStorageDirectory() + "/Pictures";
|
||||
baseFolder = new File(folderPath);
|
||||
}
|
||||
}
|
||||
|
||||
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
|
||||
PinnedAlbumDbHelper pinnedDbHelper = PinnedAlbumDbHelper.getInstance(this);
|
||||
|
||||
// 获取带排序号的置顶路径映射(已按排序号排序)
|
||||
java.util.LinkedHashMap<String, Integer> pinnedPathMap = pinnedDbHelper.getPinnedPathsWithSortOrder();
|
||||
Set<String> pinnedPaths = pinnedPathMap.keySet();
|
||||
|
||||
ArrayList<Album> albums = new ArrayList<Album>();
|
||||
|
||||
File[] subfolders = baseFolder.listFiles(new FileFilter() {
|
||||
@Override
|
||||
public boolean accept(File file) {
|
||||
return file.isDirectory();
|
||||
}
|
||||
});
|
||||
if (subfolders != null) {
|
||||
for (File subfolder : subfolders) {
|
||||
String albumPath = subfolder.getAbsolutePath();
|
||||
String coverPath = coverDbHelper.getCover(albumPath);
|
||||
Uri coverUri = null;
|
||||
if (coverPath != null) {
|
||||
File coverFile = new File(coverPath);
|
||||
if (coverFile.exists()) {
|
||||
coverUri = Uri.fromFile(coverFile);
|
||||
} else {
|
||||
coverUri = getUriFromPath(coverPath);
|
||||
}
|
||||
}
|
||||
ArrayList<Uri> allImages = getImagesInFolder(albumPath);
|
||||
if (coverUri == null && !allImages.isEmpty()) {
|
||||
coverUri = allImages.get(0);
|
||||
}
|
||||
if (coverUri != null || !allImages.isEmpty()) {
|
||||
int imageCount = allImages.size();
|
||||
albums.add(new Album(subfolder.getName(), albumPath, coverUri, imageCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按照排序号顺序添加置顶相册
|
||||
ArrayList<Album> pinned = new ArrayList<Album>();
|
||||
ArrayList<Album> unpinned = new ArrayList<Album>();
|
||||
|
||||
// 先按照pinnedPathMap的顺序(排序号顺序)添加置顶相册
|
||||
for (String pinnedPath : pinnedPathMap.keySet()) {
|
||||
for (Album album : albums) {
|
||||
if (album.getPath().equals(pinnedPath)) {
|
||||
pinned.add(album);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加非置顶相册
|
||||
for (Album album : albums) {
|
||||
if (!pinnedPaths.contains(album.getPath())) {
|
||||
unpinned.add(album);
|
||||
}
|
||||
}
|
||||
|
||||
pinned.addAll(unpinned);
|
||||
return pinned;
|
||||
}
|
||||
|
||||
private Uri getUriFromPath(String path) {
|
||||
String[] projection = { MediaStore.Images.Media._ID };
|
||||
String selection = MediaStore.Images.Media.DATA + " = ?";
|
||||
String[] selectionArgs = { path };
|
||||
Cursor cursor = getContentResolver().query(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
projection, selection, selectionArgs, null);
|
||||
if (cursor != null) {
|
||||
try {
|
||||
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));
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ArrayList<Uri> getImagesInFolder(String folderPath) {
|
||||
ArrayList<Uri> imageUrls = new ArrayList<Uri>();
|
||||
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";
|
||||
|
||||
Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder);
|
||||
if (cursor != null) {
|
||||
try {
|
||||
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));
|
||||
imageUrls.add(contentUri);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return imageUrls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_main, menu);
|
||||
MenuItem debugItem = menu.findItem(R.id.action_debug);
|
||||
if (debugItem != null) {
|
||||
debugItem.setVisible(App.isDebugging());
|
||||
}
|
||||
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) {
|
||||
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()) {
|
||||
loadAlbums();
|
||||
}
|
||||
if (adapter != null) {
|
||||
adapter.refreshBg();
|
||||
adapter.refreshPinned();
|
||||
adapter.refreshCover();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
unregisterReceiver(coverUpdatedReceiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
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;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
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 = 2;
|
||||
private static final String TABLE_NAME = "pinned_albums";
|
||||
private static final String COLUMN_PATH = "album_path";
|
||||
private static final String COLUMN_SORT_ORDER = "sort_order";
|
||||
|
||||
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ COLUMN_PATH + " TEXT PRIMARY KEY, "
|
||||
+ COLUMN_SORT_ORDER + " INTEGER DEFAULT 0)";
|
||||
|
||||
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) {
|
||||
if (oldVersion < 2) {
|
||||
// 创建新表
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME + "_new");
|
||||
db.execSQL("CREATE TABLE " + TABLE_NAME + "_new ("
|
||||
+ COLUMN_PATH + " TEXT PRIMARY KEY, "
|
||||
+ COLUMN_SORT_ORDER + " INTEGER DEFAULT 0)");
|
||||
|
||||
// 迁移旧数据,为现有记录分配排序号(按原始顺序)
|
||||
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH}, null, null, null, null, null);
|
||||
int sortOrder = 1;
|
||||
while (cursor.moveToNext()) {
|
||||
String path = cursor.getString(0);
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_PATH, path);
|
||||
values.put(COLUMN_SORT_ORDER, sortOrder++);
|
||||
db.insert(TABLE_NAME + "_new", null, values);
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
// 删除旧表,重命名新表
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||
db.execSQL("ALTER TABLE " + TABLE_NAME + "_new RENAME TO " + TABLE_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
public void pinAlbum(String albumPath) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
// 将所有现有置顶数据的排序号加1
|
||||
db.execSQL("UPDATE " + TABLE_NAME + " SET " + COLUMN_SORT_ORDER + " = " + COLUMN_SORT_ORDER + " + 1");
|
||||
|
||||
// 插入新数据,排序号为1
|
||||
ContentValues insertValues = new ContentValues();
|
||||
insertValues.put(COLUMN_PATH, albumPath);
|
||||
insertValues.put(COLUMN_SORT_ORDER, 1);
|
||||
db.insertWithOnConflict(TABLE_NAME, null, insertValues, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
|
||||
// 查询所有置顶相册的排序号并输出调试信息
|
||||
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH, COLUMN_SORT_ORDER},
|
||||
null, null, null, null, COLUMN_SORT_ORDER + " ASC");
|
||||
StringBuilder sb = new StringBuilder("All pinned albums after pin:\n");
|
||||
while (cursor.moveToNext()) {
|
||||
String path = cursor.getString(0);
|
||||
int sortOrder = cursor.getInt(1);
|
||||
sb.append(" [").append(sortOrder).append("] ").append(path).append("\n");
|
||||
}
|
||||
cursor.close();
|
||||
LogUtils.d(TAG, sb.toString());
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public void unpinAlbum(String albumPath) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
// 获取要取消置顶的相册的排序号
|
||||
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_SORT_ORDER},
|
||||
COLUMN_PATH + " = ?", new String[]{albumPath}, null, null, null);
|
||||
if (cursor.moveToFirst()) {
|
||||
int sortNumber = cursor.getInt(0);
|
||||
cursor.close();
|
||||
|
||||
// 将排序号大于该排序号的记录减1
|
||||
db.execSQL("UPDATE " + TABLE_NAME + " SET " + COLUMN_SORT_ORDER + " = " + COLUMN_SORT_ORDER + " - 1 WHERE " + COLUMN_SORT_ORDER + " > ?", new Object[]{sortNumber});
|
||||
|
||||
// 删除该相册的置顶记录
|
||||
db.delete(TABLE_NAME, COLUMN_PATH + " = ?", new String[]{albumPath});
|
||||
|
||||
LogUtils.d(TAG, "unpinAlbum: " + albumPath + ", sort number was: " + sortNumber);
|
||||
} else {
|
||||
cursor.close();
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
COLUMN_SORT_ORDER + " ASC");
|
||||
String[] paths = new String[cursor.getCount()];
|
||||
int i = 0;
|
||||
while (cursor.moveToNext()) {
|
||||
paths[i++] = cursor.getString(0);
|
||||
}
|
||||
cursor.close();
|
||||
return paths;
|
||||
}
|
||||
|
||||
public LinkedHashMap<String, Integer> getPinnedPathsWithSortOrder() {
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH, COLUMN_SORT_ORDER},
|
||||
null, null, null, null, COLUMN_SORT_ORDER + " ASC");
|
||||
LinkedHashMap<String, Integer> map = new LinkedHashMap<String, Integer>();
|
||||
while (cursor.moveToNext()) {
|
||||
String path = cursor.getString(0);
|
||||
int sortOrder = cursor.getInt(1);
|
||||
map.put(path, sortOrder);
|
||||
}
|
||||
cursor.close();
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -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 final 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;
|
||||
}
|
||||
}
|
||||
+29
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="78.5885"
|
||||
android:endY="90.9159"
|
||||
android:startX="48.7653"
|
||||
android:startY="61.0927"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1" />
|
||||
</vector>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillColor="#26A69A"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
</vector>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:clickable="true">
|
||||
<item
|
||||
android:left="0dp"
|
||||
android:top="0dp"
|
||||
android:right="0dp"
|
||||
android:bottom="0dp"
|
||||
android:drawable="@drawable/mi"/>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:clickable="true">
|
||||
<item
|
||||
android:left="0dp"
|
||||
android:top="0dp"
|
||||
android:right="0dp"
|
||||
android:bottom="0dp"
|
||||
android:drawable="@drawable/mi_blankbackground"/>
|
||||
</layer-list>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user