diff --git a/gallery/README.md b/gallery/README.md new file mode 100644 index 0000000..33637b0 --- /dev/null +++ b/gallery/README.md @@ -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) +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/) + +#### 参考文档 diff --git a/gallery/app_update_description.txt b/gallery/app_update_description.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/gallery/app_update_description.txt @@ -0,0 +1 @@ + diff --git a/gallery/build.gradle b/gallery/build.gradle new file mode 100644 index 0000000..7d8c427 --- /dev/null +++ b/gallery/build.gradle @@ -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 23 + // 适配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.15.9' + api 'cc.winboll.studio:libappbase:15.15.21' + + // 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']) +} diff --git a/gallery/build.properties b/gallery/build.properties new file mode 100644 index 0000000..167b855 --- /dev/null +++ b/gallery/build.properties @@ -0,0 +1,8 @@ +#Created by .winboll/winboll_app_build.gradle +#Sat May 02 10:32:03 CST 2026 +stageCount=16 +libraryProject= +baseVersion=15.0 +publishVersion=15.0.15 +buildCount=19 +baseBetaVersion=15.0.16 diff --git a/gallery/proguard-rules.pro b/gallery/proguard-rules.pro new file mode 100644 index 0000000..a18de74 --- /dev/null +++ b/gallery/proguard-rules.pro @@ -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 ; +} + +# 保留 native 方法(避免JNI调用失败) +-keepclasseswithmembernames class * { + native ; +} + +# 保留注解和泛型(避免反射/序列化异常) +-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 { + (); +} +-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 ; +} + +# 米盟 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 + diff --git a/gallery/src/beta/AndroidManifest.xml b/gallery/src/beta/AndroidManifest.xml new file mode 100644 index 0000000..ee78d9f --- /dev/null +++ b/gallery/src/beta/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/gallery/src/beta/res/values/strings.xml b/gallery/src/beta/res/values/strings.xml new file mode 100644 index 0000000..9a532e7 --- /dev/null +++ b/gallery/src/beta/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + + Gallery+ + diff --git a/gallery/src/main/AndroidManifest.xml b/gallery/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e806f31 --- /dev/null +++ b/gallery/src/main/AndroidManifest.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/AboutActivity.java b/gallery/src/main/java/cc/winboll/studio/gallery/AboutActivity.java new file mode 100644 index 0000000..f9afe0e --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/AboutActivity.java @@ -0,0 +1,51 @@ +package cc.winboll.studio.gallery; + +import android.os.Bundle; +import android.view.View; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.models.APPInfo; +import cc.winboll.studio.libappbase.views.AboutView; + +public class AboutActivity extends AppCompatActivity { + + public static final String TAG = "AboutActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_about); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); + + AboutView aboutView = findViewById(R.id.aboutview); + aboutView.setAPPInfo(genDefaultAppInfo()); + } + + private APPInfo genDefaultAppInfo() { + LogUtils.d(TAG, "genDefaultAppInfo() 调用"); + String branchName = "gallery"; + APPInfo appInfo = new APPInfo(); + appInfo.setAppName("Gallery"); + appInfo.setAppIcon(R.drawable.ic_winboll); + appInfo.setAppDescription(getString(R.string.app_description)); + appInfo.setAppGitName("WinBoLL"); + appInfo.setAppGitOwner("Studio"); + appInfo.setAppGitAPPBranch(branchName); + appInfo.setAppGitAPPSubProjectFolder(branchName); + appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=Gallery"); + appInfo.setAppAPKName("Gallery"); + appInfo.setAppAPKFolderName("Gallery"); + LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成"); + return appInfo; + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/Album.java b/gallery/src/main/java/cc/winboll/studio/gallery/Album.java new file mode 100644 index 0000000..42aaa02 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/Album.java @@ -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; } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/AlbumActivity.java b/gallery/src/main/java/cc/winboll/studio/gallery/AlbumActivity.java new file mode 100644 index 0000000..a9ccac2 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/AlbumActivity.java @@ -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 urls, ArrayList 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 imageUrls = new ArrayList<>(); + ArrayList 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(); + } + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/AlbumAdapter.java b/gallery/src/main/java/cc/winboll/studio/gallery/AlbumAdapter.java new file mode 100644 index 0000000..fa435bd --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/AlbumAdapter.java @@ -0,0 +1,255 @@ +package cc.winboll.studio.gallery; + +import android.net.Uri; +import android.provider.MediaStore; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +import cc.winboll.studio.libappbase.LogUtils; + +public class AlbumAdapter extends RecyclerView.Adapter { + public static final String TAG = "AlbumAdapter"; + private ArrayList 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 albums) { + this.albums = sortAlbums(albums); + LogUtils.d(TAG, "setData: " + albums.size() + " albums"); + notifyDataSetChanged(); + } + + private ArrayList sortAlbums(ArrayList list) { + if (pinnedDbHelper == null || list == null || list.isEmpty()) { + return list; + } + ArrayList pinned = new ArrayList<>(); + ArrayList unpinned = new ArrayList<>(); + for (Album album : list) { + if (pinnedDbHelper.isPinned(album.getPath())) { + pinned.add(album); + } else { + unpinned.add(album); + } + } + pinned.addAll(unpinned); + return pinned; + } + + public void setContext(android.content.Context context) { + prefs = new Preferences(context); + bgType = prefs.getBgType(); + pinnedDbHelper = PinnedAlbumDbHelper.getInstance(context); + } + + public void refreshBg() { + if (prefs != null) { + bgType = prefs.getBgType(); + } + notifyDataSetChanged(); + } + + public void refreshCover() { + notifyDataSetChanged(); + } + + public void refreshPinned() { + if (pinnedDbHelper != null && albums != null && !albums.isEmpty()) { + ArrayList pinned = new ArrayList<>(); + ArrayList unpinned = new ArrayList<>(); + for (Album album : albums) { + if (pinnedDbHelper.isPinned(album.getPath())) { + pinned.add(album); + } else { + unpinned.add(album); + } + } + Comparator nameComparator = new Comparator() { + @Override + public int compare(Album a1, Album a2) { + return a1.getName().compareToIgnoreCase(a2.getName()); + } + }; + Collections.sort(pinned, nameComparator); + Collections.sort(unpinned, nameComparator); + albums.clear(); + albums.addAll(pinned); + albums.addAll(unpinned); + notifyDataSetChanged(); + } + } + +private void showContextMenu(View view, final Album album) { + android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext()); + builder.setTitle(album.getName()); + final boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(album.getPath()); + String[] items = isPinned ? new String[]{"取消置顶"} : new String[]{"置顶"}; + builder.setItems(items, new android.content.DialogInterface.OnClickListener() { + @Override + public void onClick(android.content.DialogInterface dialog, int which) { + if (pinnedDbHelper != null) { + if (which == 0) { + if (isPinned) { + pinnedDbHelper.unpinAlbum(album.getPath()); + } else { + pinnedDbHelper.pinAlbum(album.getPath()); + } + refreshPinned(); + } + } + } + }); + builder.show(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_album, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, final int position) { + final Album album = albums.get(position); + LogUtils.d(TAG, "bind: " + album.getName() + ", cover=" + album.getCoverUri()); + + holder.coverImage.setBackgroundResource(getBgRes()); + + boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(album.getPath()); + holder.pinIcon.setVisibility(isPinned ? View.VISIBLE : View.GONE); + + holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + showContextMenu(holder.itemView, album); + return true; + } + }); + + holder.albumName.setText(album.getName()); + holder.imageCount.setText(album.getImageCount() + " photos"); + + holder.itemView.post(new Runnable() { + @Override + public void run() { + if (!coverSizeReported && prefs != null) { + int savedWidth = prefs.getCoverWidth(); + if (savedWidth == AlbumAdapter.DEFAULT_COVER_WIDTH) { + int width = holder.coverImage.getWidth(); + int height = holder.coverImage.getHeight(); + if (width > 0 && height > 0) { + prefs.setCoverWidth(width); + prefs.setCoverHeight(height); + float ratio = (float) width / height; + prefs.setCoverRatio(ratio); + coverSizeReported = true; + if (coverSizeListener != null) { + coverSizeListener.onCoverSize(width, height); + } + } + } + } + } + }); + + holder.itemView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + listener.onAlbumClick(album); + } + } + }); + + Uri coverUri = album.getCoverUri(); + if (coverUri != null) { + String uriString = coverUri.toString(); + LogUtils.d(TAG, "uri scheme: " + coverUri.getScheme() + ", path: " + uriString); + + if ("file".equals(coverUri.getScheme())) { + File coverFile = new File(coverUri.getPath()); + if (coverFile.exists()) { + Glide.with(holder.coverImage.getContext()) + .load(coverFile) + .fitCenter() + .into(holder.coverImage); + return; + } + } + + Glide.with(holder.coverImage.getContext()) + .load(coverUri) + .fitCenter() + .into(holder.coverImage); + } + } + + @Override + public int getItemCount() { + return albums.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + ImageView coverImage; + ImageView pinIcon; + TextView albumName; + TextView imageCount; + ViewHolder(View itemView) { + super(itemView); + coverImage = itemView.findViewById(R.id.album_cover); + pinIcon = itemView.findViewById(R.id.pin_icon); + albumName = itemView.findViewById(R.id.album_name); + imageCount = itemView.findViewById(R.id.image_count); + } + } +} diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/AlbumCoverDbHelper.java b/gallery/src/main/java/cc/winboll/studio/gallery/AlbumCoverDbHelper.java new file mode 100644 index 0000000..3a74d25 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/AlbumCoverDbHelper.java @@ -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); + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/CropActivity.java b/gallery/src/main/java/cc/winboll/studio/gallery/CropActivity.java new file mode 100644 index 0000000..7463937 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/CropActivity.java @@ -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; + } + } +} diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/CropCanvasView.java b/gallery/src/main/java/cc/winboll/studio/gallery/CropCanvasView.java new file mode 100644 index 0000000..372e2fe --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/CropCanvasView.java @@ -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); + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/CropOverlayView.java b/gallery/src/main/java/cc/winboll/studio/gallery/CropOverlayView.java new file mode 100644 index 0000000..9d01dc3 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/CropOverlayView.java @@ -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; + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/GlobalWinBoLLApplication.java b/gallery/src/main/java/cc/winboll/studio/gallery/GlobalWinBoLLApplication.java new file mode 100644 index 0000000..a79f82d --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/GlobalWinBoLLApplication.java @@ -0,0 +1,47 @@ +package cc.winboll.studio.gallery; + +import cc.winboll.studio.libaes.utils.WinBoLLActivityManager; +import cc.winboll.studio.libappbase.GlobalApplication; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.gallery.utils.BackgroundUtils; + +/** + * @Author 豆包&ZhanGSKen + * @Date 2026/04/24 15:23 + */ +public class GlobalWinBoLLApplication extends GlobalApplication { + + public static final String TAG = "GlobalWinBoLLApplication"; + + +@Override + public void onCreate() { + super.onCreate(); + LogUtils.d(TAG, "onCreate"); + setIsDebugging(BuildConfig.DEBUG); + //setIsDebugging(false); + + WinBoLLActivityManager.init(this); + + BackgroundUtils.initFromPreferences(this); + + // 初始化 Toast 框架 + ToastUtils.init(this); + // 设置 Toast 布局样式 + //ToastUtils.setView(R.layout.view_toast); + //ToastUtils.setStyle(new WhiteToastStyle()); + //ToastUtils.setGravity(Gravity.BOTTOM, 0, 200); + + //CrashHandler.getInstance().registerGlobal(this); + //CrashHandler.getInstance().registerPart(this); + } + +@Override + public void onTerminate() { + super.onTerminate(); + LogUtils.d(TAG, "onTerminate"); + ToastUtils.release(); + } + +} diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/ImageAdapter.java b/gallery/src/main/java/cc/winboll/studio/gallery/ImageAdapter.java new file mode 100644 index 0000000..833a307 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/ImageAdapter.java @@ -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 { + public static final String TAG = "ImageAdapter"; + private ArrayList imageUrls = new ArrayList<>(); + private ArrayList 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 urls, ArrayList paths); + } + + public void setOnImageClickListener(OnImageClickListener listener) { + this.listener = listener; + } + + public void setData(ArrayList urls, ArrayList 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 pinnedUrls = new ArrayList<>(); + ArrayList pinnedPaths = new ArrayList<>(); + ArrayList unpinnedUrls = new ArrayList<>(); + ArrayList unpinnedPaths = new ArrayList<>(); + for (int i = 0; i < imagePaths.size(); i++) { + String path = imagePaths.get(i); + if (pinnedDbHelper.isPinned(path)) { + pinnedUrls.add(imageUrls.get(i)); + pinnedPaths.add(path); + } else { + unpinnedUrls.add(imageUrls.get(i)); + unpinnedPaths.add(path); + } + } + + imageUrls.clear(); + imagePaths.clear(); + imageUrls.addAll(pinnedUrls); + imageUrls.addAll(unpinnedUrls); + imagePaths.addAll(pinnedPaths); + imagePaths.addAll(unpinnedPaths); + } + + public void setContext(android.content.Context context) { + prefs = new Preferences(context); + bgType = prefs.getBgType(); + pinnedDbHelper = PinnedImageDbHelper.getInstance(context); + coverDbHelper = AlbumCoverDbHelper.getInstance(context); + } + + public void setAlbumPath(String albumPath) { + this.albumPath = albumPath; + } + + public int getCropWidth() { + return prefs != null ? prefs.getCoverWidth() : 240; + } + + public int getCropHeight() { + return prefs != null ? prefs.getCoverHeight() : 120; + } + + public void refreshBg() { + if (prefs != null) { + bgType = prefs.getBgType(); + notifyDataSetChanged(); + } + } + + public void refreshPinned() { + if (pinnedDbHelper == null || imagePaths.isEmpty()) { + return; + } + sortPinnedFirst(); + notifyDataSetChanged(); + } + + private void showContextMenu(final View view, final int position) { + final String imagePath = imagePaths.get(position); + final Uri imageUri = imageUrls.get(position); + final boolean[] isPinned = {pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath)}; + final boolean[] isCover = {false}; + if (coverDbHelper != null && albumPath != null) { + String originalPath = coverDbHelper.getOriginalImagePath(albumPath); + isCover[0] = originalPath != null && originalPath.equals(imagePath); + } + + android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext()); + builder.setTitle("Image"); + + String[] items; + if (isPinned[0]) { + if (isCover[0]) { + items = new String[]{"取消置顶", "调整封面", "取消封面"}; + } else { + items = new String[]{"取消置顶", "设置为封面"}; + } + } else { + if (isCover[0]) { + items = new String[]{"置顶", "调整封面", "取消封面"}; + } else { + items = new String[]{"置顶", "设置为封面"}; + } + } + + builder.setItems(items, new android.content.DialogInterface.OnClickListener() { + @Override + public void onClick(android.content.DialogInterface dialog, int which) { + if (which == 0) { + if (pinnedDbHelper != null) { + if (isPinned[0]) { + pinnedDbHelper.unpinImage(imagePath); + } else { + pinnedDbHelper.pinImage(imagePath); + } + refreshPinned(); + } + } else if (which == 1) { + if (coverDbHelper != null && albumPath != null) { + Intent cropIntent = new Intent(view.getContext(), CropActivity.class); + cropIntent.putExtra(CropActivity.EXTRA_IMAGE_URI, imageUri); + cropIntent.putExtra(CropActivity.EXTRA_IMAGE_PATH, imagePath); + cropIntent.putExtra(CropActivity.EXTRA_ALBUM_PATH, albumPath); + cropIntent.putExtra(CropActivity.EXTRA_CROP_WIDTH, getCropWidth()); + cropIntent.putExtra(CropActivity.EXTRA_CROP_HEIGHT, getCropHeight()); + ((AlbumActivity) view.getContext()).startActivityForResult(cropIntent, 100); + } + } else if (which == 2) { + if (coverDbHelper != null && albumPath != null && isCover[0]) { + coverDbHelper.deleteCover(albumPath); + notifyDataSetChanged(); + } + } + } + }); + builder.show(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_gallery, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, final int position) { + holder.imageView.setBackgroundResource(getBgRes()); + + Glide.with(holder.imageView.getContext()) + .load(imageUrls.get(position)) + .centerCrop() + .into(holder.imageView); + + final ArrayList urls = imageUrls; + final ArrayList 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); + } + } +} diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/ImagePagerAdapter.java b/gallery/src/main/java/cc/winboll/studio/gallery/ImagePagerAdapter.java new file mode 100644 index 0000000..e420f9b --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/ImagePagerAdapter.java @@ -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 imageUrls; + + public ImagePagerAdapter(ArrayList 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; + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/ImageViewerActivity.java b/gallery/src/main/java/cc/winboll/studio/gallery/ImageViewerActivity.java new file mode 100644 index 0000000..2bb7aac --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/ImageViewerActivity.java @@ -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 imageUrls; + private ArrayList 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(); + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/MainActivity.java b/gallery/src/main/java/cc/winboll/studio/gallery/MainActivity.java new file mode 100644 index 0000000..bb1e352 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/MainActivity.java @@ -0,0 +1,419 @@ +package cc.winboll.studio.gallery; + +import android.Manifest; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.media.MediaScannerConnection; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.provider.MediaStore; +import android.provider.Settings; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import cc.winboll.studio.gallery.AlbumAdapter.OnAlbumClickListener; +import cc.winboll.studio.gallery.utils.BackgroundUtils; +import cc.winboll.studio.libappbase.LogActivity; +import cc.winboll.studio.libappbase.LogUtils; +import com.a4455jkjh.colorpicker.ColorPickerDialog; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import java.io.File; +import java.io.FileFilter; +import java.io.FilenameFilter; +import java.util.ArrayList; + +public class MainActivity extends AppCompatActivity { + public static final String TAG = "MainActivity"; + private static final int PERMISSION_REQUEST_CODE = 100; + private static final int MANAGE_PERMISSION_REQUEST_CODE = 101; + private RecyclerView recyclerView; + private AlbumAdapter adapter; + private Preferences prefs; + private FloatingActionButton fabScrollTop; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + LogUtils.d(TAG, "onCreate"); + + View content = findViewById(android.R.id.content); + if (content != null) { + content.setBackground(BackgroundUtils.getInstance().getDrawable()); + } + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + prefs = new Preferences(this); + + recyclerView = findViewById(R.id.recycler_view); + recyclerView.setLayoutManager(new GridLayoutManager(this, 2)); + adapter = new AlbumAdapter(); + adapter.setContext(this); + recyclerView.setAdapter(adapter); + + fabScrollTop = findViewById(R.id.fab_scroll_top); + fabScrollTop.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + recyclerView.scrollToPosition(0); + } + }); + + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager(); + if (layoutManager != null) { + int firstVisible = layoutManager.findFirstVisibleItemPosition(); + if (firstVisible > 0) { + fabScrollTop.setVisibility(View.VISIBLE); + } else { + fabScrollTop.setVisibility(View.GONE); + } + } + } + }); + + adapter.setOnAlbumClickListener(new OnAlbumClickListener() { + @Override + public void onAlbumClick(Album album) { + Intent intent = new Intent(MainActivity.this, AlbumActivity.class); + intent.putExtra(AlbumActivity.EXTRA_ALBUM_PATH, album.getPath()); + intent.putExtra(AlbumActivity.EXTRA_ALBUM_NAME, album.getName()); + startActivity(intent); + } + }); + + checkAndRequestPermissions(); + } + + private void checkAndRequestPermissions() { + LogUtils.i(TAG, "checkAndRequestPermissions"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (!Environment.isExternalStorageManager()) { + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); + intent.setData(Uri.parse("package:" + getPackageName())); + startActivityForResult(intent, MANAGE_PERMISSION_REQUEST_CODE); + } catch (Exception e) { + Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); + startActivityForResult(intent, MANAGE_PERMISSION_REQUEST_CODE); + } + return; + } + } + + if (checkPermission()) { + loadAlbums(); + } else { + requestPermission(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == MANAGE_PERMISSION_REQUEST_CODE) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (Environment.isExternalStorageManager()) { + loadAlbums(); + } else { + Toast.makeText(this, "Permission required", Toast.LENGTH_SHORT).show(); + } + } + } + } + + private boolean checkPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return Environment.isExternalStorageManager(); + } + return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED; + } + + private void requestPermission() { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, + PERMISSION_REQUEST_CODE); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == PERMISSION_REQUEST_CODE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + loadAlbums(); + } else { + Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show(); + } + } + } + + private void loadAlbums() { + LogUtils.d(TAG, "loadAlbums"); + String folderPath = prefs.getFolderPath(); + File baseFolder = new File(folderPath); + LogUtils.d(TAG, "baseFolder: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists()); + + if (!baseFolder.exists() || !baseFolder.isDirectory()) { + folderPath = Preferences.getDefaultPath(); + baseFolder = new File(folderPath); + LogUtils.d(TAG, "try default: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists()); + if (!baseFolder.exists()) { + folderPath = Environment.getExternalStorageDirectory() + "/Pictures"; + baseFolder = new File(folderPath); + LogUtils.d(TAG, "try Pictures: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists()); + } + } + + AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this); + ArrayList albums = new ArrayList<>(); + + FileFilter directoryFilter = new FileFilter() { + @Override + public boolean accept(File file) { + return file.isDirectory(); + } + }; + File[] subfolders = baseFolder.listFiles(directoryFilter); + LogUtils.d(TAG, "subfolders: " + (subfolders != null ? subfolders.length : 0)); + if (subfolders != null) { + for (File subfolder : subfolders) { + LogUtils.d(TAG, "scanning folder: " + subfolder.getName()); + String albumPath = subfolder.getAbsolutePath(); + String coverPath = coverDbHelper.getCover(albumPath); + LogUtils.d(TAG, "loadAlbums: album=" + albumPath + ", coverPath=" + coverPath); + Uri coverUri = null; + if (coverPath != null) { + File coverFile = new File(coverPath); + if (coverFile.exists()) { + coverUri = Uri.fromFile(coverFile); + LogUtils.d(TAG, "loadAlbums: cover from file=" + coverFile.getAbsolutePath()); + } else { + coverUri = getUriFromPath(coverPath); + LogUtils.d(TAG, "loadAlbums: cover from media store path=" + coverPath); + } + } + if (coverUri == null) { + ArrayList images = getImagesInFolder(albumPath); + if (!images.isEmpty()) { + coverUri = images.get(0); + } + } + ArrayList allImages = getImagesInFolder(albumPath); + if (coverUri != null || !allImages.isEmpty()) { + if (coverUri == null && !allImages.isEmpty()) { + coverUri = allImages.get(0); + } + int imageCount = allImages.size(); + albums.add(new Album(subfolder.getName(), albumPath, coverUri, imageCount)); + LogUtils.d(TAG, "album added: " + subfolder.getName() + ", " + imageCount + " images"); + } + } + } + + if (albums.isEmpty()) { + Toast.makeText(this, R.string.no_images_found, Toast.LENGTH_SHORT).show(); + LogUtils.i(TAG, "No albums found"); + } + adapter.setData(albums); + LogUtils.d(TAG, "Loaded " + albums.size() + " albums"); + } + + private Uri getUriFromPath(String path) { + String[] projection = { MediaStore.Images.Media._ID }; + String selection = MediaStore.Images.Media.DATA + " = ?"; + String[] selectionArgs = { path }; + try (Cursor cursor = getContentResolver().query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + projection, selection, selectionArgs, null)) { + if (cursor != null) { + if (cursor.moveToFirst()) { + long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)); + return Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id)); + } + } + } + return null; + } + + private ArrayList getImagesInFolder(String folderPath) { + ArrayList imageUrls = new ArrayList<>(); + ContentResolver contentResolver = getContentResolver(); + Uri collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + + String selection = MediaStore.Images.Media.DATA + " LIKE ?"; + String[] selectionArgs = new String[]{folderPath + "/%"}; + String sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC"; + + LogUtils.d(TAG, "getImagesInFolder: " + folderPath); + + try (Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder)) { + if (cursor != null) { + LogUtils.d(TAG, "cursor count: " + cursor.getCount()); + int dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); + while (cursor.moveToNext()) { + String path = cursor.getString(dataColumn); + if (path != null) { + long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)); + Uri contentUri = Uri.withAppendedPath(collection, String.valueOf(id)); + LogUtils.d(TAG, "image: id=" + id + ", path=" + path); + imageUrls.add(contentUri); + } + } + } + } + LogUtils.d(TAG, "found " + imageUrls.size() + " images"); + return imageUrls; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + int id = item.getItemId(); + if (id == R.id.action_mi_gallery) { + Toast.makeText(this, "Gallery clicked", Toast.LENGTH_SHORT).show(); + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_APP_GALLERY); + startActivity(intent); + return true; + } else if (id == R.id.action_change_bg_color) { + //Toast.makeText(this, "修改背景颜色", Toast.LENGTH_SHORT).show(); + if (BackgroundUtils.DrawableType.COLOR == BackgroundUtils.getInstance().getDrawableType()) { + + ColorPickerDialog dlg = new ColorPickerDialog(this, BackgroundUtils.getInstance().getColor()); + dlg.setOnColorChangedListener(new com.a4455jkjh.colorpicker.view.OnColorChangedListener() { + + @Override + public void beforeColorChanged() { + } + + @Override + public void onColorChanged(int color) { + BackgroundUtils.getInstance().initFromColor(MainActivity.this, color); + View content = findViewById(android.R.id.content); + if (content != null) { + content.setBackground(BackgroundUtils.getInstance().getDrawable()); + } + } + + @Override + public void afterColorChanged() { + } + }); + dlg.show(); + } + return true; + } else if (id == R.id.action_settings) { + startActivity(new Intent(this, SettingsActivity.class)); + return true; + } else if (id == R.id.action_about) { + startActivity(new Intent(this, AboutActivity.class)); + return true; + } else if (id == R.id.action_trash) { + startActivity(new Intent(this, TrashActivity.class)); + return true; + } else if (id == R.id.action_debug) { + LogActivity.startLogActivity(this); + return true; + } + return super.onOptionsItemSelected(item); + } + + private BroadcastReceiver coverUpdatedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Preferences.ACTION_COVER_UPDATED.equals(intent.getAction())) { + loadAlbums(); + } + } + }; + + @Override + protected void onResume() { + super.onResume(); + registerReceiver(coverUpdatedReceiver, new IntentFilter(Preferences.ACTION_COVER_UPDATED)); + if (checkPermission()) { + scanMediaStore(); + loadAlbums(); + } + if (adapter != null) { + adapter.refreshBg(); + adapter.refreshPinned(); + adapter.refreshCover(); + } + } + + @Override + protected void onPause() { + super.onPause(); + unregisterReceiver(coverUpdatedReceiver); + } + + private void scanMediaStore() { + String folderPath = prefs.getFolderPath(); + File baseFolder = new File(folderPath); + if (baseFolder.exists() && baseFolder.isDirectory()) { + File[] subfolders = baseFolder.listFiles(new FileFilter() { + @Override + public boolean accept(File file) { + return file.isDirectory(); + } + }); + if (subfolders != null) { + ArrayList paths = new ArrayList<>(); + for (File subfolder : subfolders) { + File[] images = subfolder.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + String lower = name.toLowerCase(); + return lower.endsWith(".jpg") || lower.endsWith(".jpeg") + || lower.endsWith(".png") || lower.endsWith(".gif") + || lower.endsWith(".webp") || lower.endsWith(".bmp"); + } + }); + if (images != null) { + for (File img : images) { + paths.add(img.getAbsolutePath()); + } + } + } + if (!paths.isEmpty()) { + LogUtils.d(TAG, "scanning " + paths.size() + " files to MediaStore"); + String[] pathArray = paths.toArray(new String[0]); + MediaScannerConnection.scanFile(this, pathArray, null, new MediaScannerConnection.OnScanCompletedListener() { + @Override + public void onScanCompleted(String path, Uri uri) { + LogUtils.d(TAG, "scanCompleted: " + path + " -> " + uri); + } + }); + } + } + } + } +} diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/PinnedAlbumDbHelper.java b/gallery/src/main/java/cc/winboll/studio/gallery/PinnedAlbumDbHelper.java new file mode 100644 index 0000000..6dc35e9 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/PinnedAlbumDbHelper.java @@ -0,0 +1,78 @@ +package cc.winboll.studio.gallery; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import cc.winboll.studio.libappbase.LogUtils; + +public class PinnedAlbumDbHelper extends SQLiteOpenHelper { + public static final String TAG = "PinnedAlbumDbHelper"; + private static final String DB_NAME = "pinned_album.db"; + private static final int DB_VERSION = 1; + private static final String TABLE_NAME = "pinned_albums"; + private static final String COLUMN_PATH = "album_path"; + + private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " (" + + COLUMN_PATH + " TEXT PRIMARY KEY)"; + + private static PinnedAlbumDbHelper dbHelper; + + public static PinnedAlbumDbHelper getInstance(Context context) { + if (dbHelper == null) { + dbHelper = new PinnedAlbumDbHelper(context.getApplicationContext()); + } + return dbHelper; + } + + public PinnedAlbumDbHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(SQL_CREATE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); + onCreate(db); + } + + public void pinAlbum(String albumPath) { + SQLiteDatabase db = getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(COLUMN_PATH, albumPath); + db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE); + LogUtils.d(TAG, "pinAlbum: " + albumPath); + } + + public void unpinAlbum(String albumPath) { + SQLiteDatabase db = getWritableDatabase(); + db.delete(TABLE_NAME, COLUMN_PATH + " = ?", new String[]{albumPath}); + LogUtils.d(TAG, "unpinAlbum: " + albumPath); + } + + public boolean isPinned(String albumPath) { + SQLiteDatabase db = getReadableDatabase(); + Cursor cursor = db.query(TABLE_NAME, null, COLUMN_PATH + " = ?", + new String[]{albumPath}, null, null, null); + boolean pinned = cursor.getCount() > 0; + cursor.close(); + return pinned; + } + + public String[] getPinnedPaths() { + SQLiteDatabase db = getReadableDatabase(); + Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH}, null, null, null, null, null); + String[] paths = new String[cursor.getCount()]; + int i = 0; + while (cursor.moveToNext()) { + paths[i++] = cursor.getString(0); + } + cursor.close(); + return paths; + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/PinnedImageDbHelper.java b/gallery/src/main/java/cc/winboll/studio/gallery/PinnedImageDbHelper.java new file mode 100644 index 0000000..460d6e9 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/PinnedImageDbHelper.java @@ -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; + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/Preferences.java b/gallery/src/main/java/cc/winboll/studio/gallery/Preferences.java new file mode 100644 index 0000000..d005cd9 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/Preferences.java @@ -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(); + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/SettingsActivity.java b/gallery/src/main/java/cc/winboll/studio/gallery/SettingsActivity.java new file mode 100644 index 0000000..c730c73 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/SettingsActivity.java @@ -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(); + } + }); + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/TrashActivity.java b/gallery/src/main/java/cc/winboll/studio/gallery/TrashActivity.java new file mode 100644 index 0000000..ab30395 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/TrashActivity.java @@ -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 items = new ArrayList(); + ArrayList uris = new ArrayList(); + + 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; + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/TrashAdapter.java b/gallery/src/main/java/cc/winboll/studio/gallery/TrashAdapter.java new file mode 100644 index 0000000..49a7cae --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/TrashAdapter.java @@ -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 { + public static final String TAG = "TrashAdapter"; + private ArrayList trashItems = new ArrayList(); + private ArrayList imageUrls = new ArrayList(); + 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 items, ArrayList uris) { + this.trashItems = items; + this.imageUrls = uris; + LogUtils.d(TAG, "setData: " + items.size() + " items"); + notifyDataSetChanged(); + } + + public long getItemId(int position) { + if (position >= 0 && position < trashItems.size()) { + return trashItems.get(position).id; + } + return -1; + } + + public String getFileName(int position) { + if (position >= 0 && position < trashItems.size()) { + return trashItems.get(position).fileName; + } + return ""; + } + + public String getOriginalPath(int position) { + if (position >= 0 && position < trashItems.size()) { + return trashItems.get(position).originalPath; + } + return ""; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_trash, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, final int position) { + if (position < imageUrls.size()) { + Glide.with(holder.imageView.getContext()) + .load(imageUrls.get(position)) + .centerCrop() + .into(holder.imageView); + } + + holder.btnRestore.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + listener.onRestoreClick(position); + } + } + }); + + holder.btnDelete.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + listener.onDeleteClick(position); + } + } + }); + } + + @Override + public int getItemCount() { + return trashItems.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + ImageView imageView; + ImageView btnRestore; + ImageView btnDelete; + ViewHolder(View itemView) { + super(itemView); + imageView = itemView.findViewById(R.id.image); + btnRestore = itemView.findViewById(R.id.btn_restore); + btnDelete = itemView.findViewById(R.id.btn_delete); + } + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/TrashDbHelper.java b/gallery/src/main/java/cc/winboll/studio/gallery/TrashDbHelper.java new file mode 100644 index 0000000..6d5cd11 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/TrashDbHelper.java @@ -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(); + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/TrashManager.java b/gallery/src/main/java/cc/winboll/studio/gallery/TrashManager.java new file mode 100644 index 0000000..92a527b --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/TrashManager.java @@ -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); + } + }); + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/ZoomContainerView.java b/gallery/src/main/java/cc/winboll/studio/gallery/ZoomContainerView.java new file mode 100644 index 0000000..7fb4bdb --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/ZoomContainerView.java @@ -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); + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/dialog/ColorPaletteDialog.java b/gallery/src/main/java/cc/winboll/studio/gallery/dialog/ColorPaletteDialog.java new file mode 100644 index 0000000..ce5e959 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/dialog/ColorPaletteDialog.java @@ -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); + } +} diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/utils/BackgroundUtils.java b/gallery/src/main/java/cc/winboll/studio/gallery/utils/BackgroundUtils.java new file mode 100644 index 0000000..f55cef8 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/utils/BackgroundUtils.java @@ -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(); + } +} diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/utils/CropBackgroundUtils.java b/gallery/src/main/java/cc/winboll/studio/gallery/utils/CropBackgroundUtils.java new file mode 100644 index 0000000..fb399f9 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/utils/CropBackgroundUtils.java @@ -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(); + } +} diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/views/BackgroundRadioButton.java b/gallery/src/main/java/cc/winboll/studio/gallery/views/BackgroundRadioButton.java new file mode 100644 index 0000000..8809e00 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/views/BackgroundRadioButton.java @@ -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; + } +} diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/views/CustomApplicationBackground.java b/gallery/src/main/java/cc/winboll/studio/gallery/views/CustomApplicationBackground.java new file mode 100644 index 0000000..729521f --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/views/CustomApplicationBackground.java @@ -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; + } +} diff --git a/gallery/src/main/res/drawable-v24/ic_launcher_foreground.xml b/gallery/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/gallery/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/gallery/src/main/res/drawable/bg_black.xml b/gallery/src/main/res/drawable/bg_black.xml new file mode 100644 index 0000000..e0fb6f9 --- /dev/null +++ b/gallery/src/main/res/drawable/bg_black.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/bg_checkerboard.xml b/gallery/src/main/res/drawable/bg_checkerboard.xml new file mode 100644 index 0000000..f89e1af --- /dev/null +++ b/gallery/src/main/res/drawable/bg_checkerboard.xml @@ -0,0 +1,30 @@ + + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/bg_circle_white.xml b/gallery/src/main/res/drawable/bg_circle_white.xml new file mode 100644 index 0000000..fb0d808 --- /dev/null +++ b/gallery/src/main/res/drawable/bg_circle_white.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/bg_color_circle.xml b/gallery/src/main/res/drawable/bg_color_circle.xml new file mode 100644 index 0000000..847f7a9 --- /dev/null +++ b/gallery/src/main/res/drawable/bg_color_circle.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/bg_color_circle_border.xml b/gallery/src/main/res/drawable/bg_color_circle_border.xml new file mode 100644 index 0000000..cc52daa --- /dev/null +++ b/gallery/src/main/res/drawable/bg_color_circle_border.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/bg_dialog.xml b/gallery/src/main/res/drawable/bg_dialog.xml new file mode 100644 index 0000000..5477c09 --- /dev/null +++ b/gallery/src/main/res/drawable/bg_dialog.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/bg_white.xml b/gallery/src/main/res/drawable/bg_white.xml new file mode 100644 index 0000000..0c561be --- /dev/null +++ b/gallery/src/main/res/drawable/bg_white.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_arrow_up.xml b/gallery/src/main/res/drawable/ic_arrow_up.xml new file mode 100644 index 0000000..aa5b4d5 --- /dev/null +++ b/gallery/src/main/res/drawable/ic_arrow_up.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_back.xml b/gallery/src/main/res/drawable/ic_back.xml new file mode 100644 index 0000000..0cdf891 --- /dev/null +++ b/gallery/src/main/res/drawable/ic_back.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_bg.xml b/gallery/src/main/res/drawable/ic_bg.xml new file mode 100644 index 0000000..0580837 --- /dev/null +++ b/gallery/src/main/res/drawable/ic_bg.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_close.xml b/gallery/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..2789d9c --- /dev/null +++ b/gallery/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_color_pick.xml b/gallery/src/main/res/drawable/ic_color_pick.xml new file mode 100644 index 0000000..538fb37 --- /dev/null +++ b/gallery/src/main/res/drawable/ic_color_pick.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_cover.xml b/gallery/src/main/res/drawable/ic_cover.xml new file mode 100644 index 0000000..05f5d75 --- /dev/null +++ b/gallery/src/main/res/drawable/ic_cover.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_delete.xml b/gallery/src/main/res/drawable/ic_delete.xml new file mode 100644 index 0000000..aa0f7a0 --- /dev/null +++ b/gallery/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_done.xml b/gallery/src/main/res/drawable/ic_done.xml new file mode 100644 index 0000000..a7672f7 --- /dev/null +++ b/gallery/src/main/res/drawable/ic_done.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_info.xml b/gallery/src/main/res/drawable/ic_info.xml new file mode 100644 index 0000000..b49a72b --- /dev/null +++ b/gallery/src/main/res/drawable/ic_info.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_launcher_background.xml b/gallery/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/gallery/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/src/main/res/drawable/ic_mi_gallery.xml b/gallery/src/main/res/drawable/ic_mi_gallery.xml new file mode 100644 index 0000000..17bc4bd --- /dev/null +++ b/gallery/src/main/res/drawable/ic_mi_gallery.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/gallery/src/main/res/drawable/ic_pin.xml b/gallery/src/main/res/drawable/ic_pin.xml new file mode 100644 index 0000000..9963da0 --- /dev/null +++ b/gallery/src/main/res/drawable/ic_pin.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_restore.xml b/gallery/src/main/res/drawable/ic_restore.xml new file mode 100644 index 0000000..f607ca2 --- /dev/null +++ b/gallery/src/main/res/drawable/ic_restore.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_share.xml b/gallery/src/main/res/drawable/ic_share.xml new file mode 100644 index 0000000..ff756ae --- /dev/null +++ b/gallery/src/main/res/drawable/ic_share.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_sort.xml b/gallery/src/main/res/drawable/ic_sort.xml new file mode 100644 index 0000000..7d37ecc --- /dev/null +++ b/gallery/src/main/res/drawable/ic_sort.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_view_gallery_outline.xml b/gallery/src/main/res/drawable/ic_view_gallery_outline.xml new file mode 100644 index 0000000..c4861fb --- /dev/null +++ b/gallery/src/main/res/drawable/ic_view_gallery_outline.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_zoom_in.xml b/gallery/src/main/res/drawable/ic_zoom_in.xml new file mode 100644 index 0000000..45a16f6 --- /dev/null +++ b/gallery/src/main/res/drawable/ic_zoom_in.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_zoom_out.xml b/gallery/src/main/res/drawable/ic_zoom_out.xml new file mode 100644 index 0000000..6fdbb4a --- /dev/null +++ b/gallery/src/main/res/drawable/ic_zoom_out.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/gallery/src/main/res/layout/activity_about.xml b/gallery/src/main/res/layout/activity_about.xml new file mode 100644 index 0000000..790b67d --- /dev/null +++ b/gallery/src/main/res/layout/activity_about.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/src/main/res/layout/activity_crop.xml b/gallery/src/main/res/layout/activity_crop.xml new file mode 100644 index 0000000..39e528e --- /dev/null +++ b/gallery/src/main/res/layout/activity_crop.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/src/main/res/layout/activity_image_viewer.xml b/gallery/src/main/res/layout/activity_image_viewer.xml new file mode 100644 index 0000000..daaf21e --- /dev/null +++ b/gallery/src/main/res/layout/activity_image_viewer.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/src/main/res/layout/activity_main.xml b/gallery/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..3a1bd64 --- /dev/null +++ b/gallery/src/main/res/layout/activity_main.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/src/main/res/layout/activity_settings.xml b/gallery/src/main/res/layout/activity_settings.xml new file mode 100644 index 0000000..24d2676 --- /dev/null +++ b/gallery/src/main/res/layout/activity_settings.xml @@ -0,0 +1,39 @@ + + + + + + + + + +