Compare commits

...

22 Commits

Author SHA1 Message Date
5a700a0808 <gallery>优化裁剪功能:更新颜色拾取逻辑、同步工具栏图标、调整布局文件 2026-04-28 19:46:23 +08:00
dad179c15f 完善剪裁窗口颜色拾取功能,工具栏图标同步更新 2026-04-28 18:31:16 +08:00
1b62ff288f <gallery>APK 15.0.9 release Publish. 2026-04-28 17:21:55 +08:00
87ad6668a4 更新 gallery 模块 libappbase 依赖到 15.15.21 2026-04-28 17:18:30 +08:00
139083c22f <gallery>APK 15.0.8 release Publish. 2026-04-28 13:58:56 +08:00
c49e68d7f1 添加裁剪信息窗口功能
- 添加信息按钮和对话框,显示画布、颜色、裁剪信息
- 显示裁剪结果预览图和拾取坐标
- 松开手指时重置拾取按钮状态
2026-04-28 13:56:08 +08:00
b035d461aa 优化剪裁窗口颜色拾取功能
- 按下手指时预览颜色,放开时确认拾取
- 拖动时实时跟随手指位置更新颜色
- 分离颜色预览和确认拾取的回调接口
2026-04-28 13:06:45 +08:00
474ddcbb3b 添加剪裁窗口颜色拾取功能
- 在工具栏添加颜色图标显示当前背景颜色
- 添加背景颜色变化监听器
- 修复画布放大时颜色拾取坐标计算
- 拾取颜色后自动退出拾取模式
2026-04-28 12:35:55 +08:00
a7e2646eca <gallery>APK 15.0.7 release Publish. 2026-04-28 10:57:44 +08:00
a12577a369 限制裁剪框放大时超出画布范围
- 双指缩放裁剪框时添加边界检测
- 裁剪框超出边界时停止放大,保持在画布内
2026-04-28 10:45:42 +08:00
990ca8e27c 修复裁剪框在画布缩放后的触摸操作
- onTouchEvent()中将触摸坐标除以containerScale进行转换
- 边界检测使用转换后的坐标buckets
- 确保单指移动裁剪框在缩放后正常工作
2026-04-28 10:26:58 +08:00
7f6cd8295c 裁剪框跟随画布控件同步缩放
- 添加containerScale属性跟踪缩放比例
- setContainerScale()同步缩放比例到裁剪框
- onDraw()中使用canvas.scale()缩放裁剪框绘制
- 角标半径按比例调整
2026-04-28 10:18:22 +08:00
ae3a0c6f64 使用进度条控制画布控件缩放
- 移除顶部放大/缩小按钮,改用底部SeekBar进度条控制
- 缩放范围0.1-5.0对应进度条0-100
- 添加setScaleFactor()方法
2026-04-28 10:01:10 +08:00
e07931fd3b 修复裁剪窗口画布控件裁剪源问题
- 添加getCanvasBitmap()方法创建包含背景的画布位图
- 裁剪源使用画布位图,与预览显示一致
2026-04-28 09:51:49 +08:00
8d62e7df21 修改裁剪窗口默认背景颜色
- CropCanvasView背景设为蓝色
- ZoomContainerView背景设为黄色
2026-04-28 09:24:57 +08:00
1cca476acd 修复剪裁窗口画布控件缩放和布局问题
- 修复ZoomContainerView缩放按钮无效问题,添加onLayout正确缩放子视图
- 修复CropCanvasView裁剪框显示不一致问题
- 更新libappbase版本到15.15.20
- 优化剪裁窗口布局
2026-04-28 09:15:43 +08:00
31ea5c8fbb 添加主窗口封面刷新机制和剪裁图片调试日志
- 添加剪裁图片保存时的调试日志输出
- 添加广播机制通知主窗口刷新封面图片
- 优化剪裁画布的显示和缩放功能
- 添加缩放按钮和ZoomContainerView支持
- 添加新的图标资源(ic_done、ic_zoom_in、ic_zoom_out)
2026-04-27 15:59:12 +08:00
e62a907378 <gallery>APK 15.0.6 release Publish. 2026-04-26 23:52:51 +08:00
c85ba8324b 添加相册集封面裁剪功能
- AlbumCoverDbHelper添加裁剪封面字段crop_path
- 数据库升级从版本1到版本2
- 添加setCoverWithCrop方法保存裁剪封面
2026-04-26 23:47:30 +08:00
3e49c33bc1 修复主窗口启动时相册集封面不显示问题
- 裁剪封面是本地文件,直接使用Uri.fromFile加载
- 文件不存在时使用MediaStore查询
2026-04-26 21:06:41 +08:00
3aab93cc4d 添加主窗口关于菜单和刷新封面功能
- 主窗口添加工具栏关于应用菜单
- AboutActivity使用AppBarLayout与主窗口一致
- 添加返回按钮到AboutActivity工具栏
- 去掉主窗口刷新菜单
- onResume时刷新相册集封面
2026-04-26 20:41:26 +08:00
90d8330798 添加应用介绍窗口资源 2026-04-26 20:03:17 +08:00
25 changed files with 1193 additions and 125 deletions

View File

@@ -109,7 +109,7 @@ dependencies {
*/ */
// WinBoLL库 nexus.winboll.cc 地址 // WinBoLL库 nexus.winboll.cc 地址
api 'cc.winboll.studio:libaes:15.15.9' api 'cc.winboll.studio:libaes:15.15.9'
api 'cc.winboll.studio:libappbase:15.15.19' api 'cc.winboll.studio:libappbase:15.15.21'
// WinBoLL备用库 jitpack.io 地址 // WinBoLL备用库 jitpack.io 地址
//api 'com.github.ZhanGSKen:AES:aes-v15.15.7' //api 'com.github.ZhanGSKen:AES:aes-v15.15.7'

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle #Created by .winboll/winboll_app_build.gradle
#Sun Apr 26 11:32:37 HKT 2026 #Tue Apr 28 19:41:44 CST 2026
stageCount=6 stageCount=10
libraryProject= libraryProject=
baseVersion=15.0 baseVersion=15.0
publishVersion=15.0.5 publishVersion=15.0.9
buildCount=0 buildCount=11
baseBetaVersion=15.0.6 baseBetaVersion=15.0.10

View File

@@ -46,6 +46,14 @@
<activity <activity
android:name=".TrashActivity" android:name=".TrashActivity"
android:label="@string/trash"/> android:label="@string/trash"/>
<activity
android:name=".CropActivity"
android:label="调整封面"/>
<activity
android:name=".AboutActivity"
android:label="@string/about"/>
<meta-data <meta-data
android:name="android.max_aspect" android:name="android.max_aspect"

View File

@@ -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;
}
}

View File

@@ -240,4 +240,12 @@ private String getSortOrder(int sortMode) {
adapter.refreshBg(); adapter.refreshBg();
} }
} }
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 100 && resultCode == RESULT_OK) {
loadImages();
}
}
} }

View File

@@ -26,6 +26,8 @@ public class AlbumAdapter extends RecyclerView.Adapter<AlbumAdapter.ViewHolder>
private Preferences prefs; private Preferences prefs;
private int bgType = 0; private int bgType = 0;
private PinnedAlbumDbHelper pinnedDbHelper; private PinnedAlbumDbHelper pinnedDbHelper;
private OnCoverSizeListener coverSizeListener;
private boolean coverSizeReported = false;
private int getBgRes() { private int getBgRes() {
switch (bgType) { switch (bgType) {
@@ -43,10 +45,21 @@ public class AlbumAdapter extends RecyclerView.Adapter<AlbumAdapter.ViewHolder>
public interface OnAlbumClickListener { public interface OnAlbumClickListener {
void onAlbumClick(Album album); 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) { public void setOnAlbumClickListener(OnAlbumClickListener listener) {
this.listener = listener; this.listener = listener;
} }
public void setOnCoverSizeListener(OnCoverSizeListener listener) {
this.coverSizeListener = listener;
}
public void setData(ArrayList<Album> albums) { public void setData(ArrayList<Album> albums) {
this.albums = sortAlbums(albums); this.albums = sortAlbums(albums);
@@ -84,6 +97,10 @@ public class AlbumAdapter extends RecyclerView.Adapter<AlbumAdapter.ViewHolder>
notifyDataSetChanged(); notifyDataSetChanged();
} }
public void refreshCover() {
notifyDataSetChanged();
}
public void refreshPinned() { public void refreshPinned() {
if (pinnedDbHelper != null && albums != null && !albums.isEmpty()) { if (pinnedDbHelper != null && albums != null && !albums.isEmpty()) {
ArrayList<Album> pinned = new ArrayList<>(); ArrayList<Album> pinned = new ArrayList<>();
@@ -162,6 +179,29 @@ private void showContextMenu(View view, final Album album) {
holder.albumName.setText(album.getName()); holder.albumName.setText(album.getName());
holder.imageCount.setText(album.getImageCount() + " photos"); 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() { holder.itemView.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@@ -176,37 +216,22 @@ private void showContextMenu(View view, final Album album) {
String uriString = coverUri.toString(); String uriString = coverUri.toString();
LogUtils.d(TAG, "uri scheme: " + coverUri.getScheme() + ", path: " + uriString); LogUtils.d(TAG, "uri scheme: " + coverUri.getScheme() + ", path: " + uriString);
// For content:// URIs, try to get the actual file path if ("file".equals(coverUri.getScheme())) {
if ("content".equals(coverUri.getScheme())) { File coverFile = new File(coverUri.getPath());
try { if (coverFile.exists()) {
android.database.Cursor cursor = holder.coverImage.getContext().getContentResolver() Glide.with(holder.coverImage.getContext())
.query(coverUri, new String[]{MediaStore.Images.Media.DATA}, null, null, null); .load(coverFile)
if (cursor != null && cursor.moveToFirst()) { .fitCenter()
int dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); .into(holder.coverImage);
String filePath = cursor.getString(dataColumn); return;
cursor.close();
if (filePath != null) {
File actualFile = new File(filePath);
LogUtils.d(TAG, "actual file: " + actualFile.getAbsolutePath() + ", exists=" + actualFile.exists());
// Use file path instead of content URI for better compatibility
Glide.with(holder.coverImage.getContext())
.load(actualFile)
.centerCrop()
.into(holder.coverImage);
return;
}
}
} catch (Exception e) {
LogUtils.e(TAG, "query error: " + e.getMessage());
} }
} }
Glide.with(holder.coverImage.getContext())
.load(coverUri)
.fitCenter()
.into(holder.coverImage);
} }
// Fallback to content URI
Glide.with(holder.coverImage.getContext())
.load(coverUri)
.centerCrop()
.into(holder.coverImage);
} }
@Override @Override

View File

@@ -10,14 +10,16 @@ import cc.winboll.studio.libappbase.LogUtils;
public class AlbumCoverDbHelper extends SQLiteOpenHelper { public class AlbumCoverDbHelper extends SQLiteOpenHelper {
public static final String TAG = "AlbumCoverDbHelper"; public static final String TAG = "AlbumCoverDbHelper";
private static final String DB_NAME = "album_cover.db"; private static final String DB_NAME = "album_cover.db";
private static final int DB_VERSION = 1; private static final int DB_VERSION = 2;
private static final String TABLE_NAME = "album_covers"; private static final String TABLE_NAME = "album_covers";
private static final String COLUMN_ALBUM_PATH = "album_path"; private static final String COLUMN_ALBUM_PATH = "album_path";
private static final String COLUMN_IMAGE_PATH = "image_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 + " (" private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_ALBUM_PATH + " TEXT PRIMARY KEY, " + COLUMN_ALBUM_PATH + " TEXT PRIMARY KEY, "
+ COLUMN_IMAGE_PATH + " TEXT)"; + COLUMN_IMAGE_PATH + " TEXT, "
+ COLUMN_CROP_PATH + " TEXT)";
private static AlbumCoverDbHelper dbHelper; private static AlbumCoverDbHelper dbHelper;
@@ -39,8 +41,24 @@ public class AlbumCoverDbHelper extends SQLiteOpenHelper {
@Override @Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); if (oldVersion < 2) {
onCreate(db); 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) { public void setCover(String albumPath, String imagePath) {
@@ -54,21 +72,60 @@ public class AlbumCoverDbHelper extends SQLiteOpenHelper {
public String getCover(String albumPath) { public String getCover(String albumPath) {
SQLiteDatabase db = getReadableDatabase(); SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_IMAGE_PATH}, Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_CROP_PATH, COLUMN_IMAGE_PATH},
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null); COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
String coverPath = null; String coverPath = null;
if (cursor != null) { if (cursor != null) {
if (cursor.moveToFirst()) { if (cursor.moveToFirst()) {
coverPath = cursor.getString(0); coverPath = cursor.getString(0);
if (coverPath == null) {
coverPath = cursor.getString(1);
}
} }
cursor.close(); cursor.close();
} }
return coverPath; 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) { public void clearCover(String albumPath) {
SQLiteDatabase db = getWritableDatabase(); SQLiteDatabase db = getWritableDatabase();
db.delete(TABLE_NAME, COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}); 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); 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);
}
} }

View File

@@ -1,5 +1,13 @@
package cc.winboll.studio.gallery; package cc.winboll.studio.gallery;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.RectF;
import android.graphics.drawable.GradientDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.RectF; import android.graphics.RectF;
@@ -8,11 +16,16 @@ import android.os.Bundle;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.view.View; import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException;
public class CropActivity extends AppCompatActivity { public class CropActivity extends AppCompatActivity {
public static final String TAG = "CropActivity"; public static final String TAG = "CropActivity";
@@ -23,6 +36,7 @@ public class CropActivity extends AppCompatActivity {
public static final String EXTRA_CROP_HEIGHT = "crop_height"; public static final String EXTRA_CROP_HEIGHT = "crop_height";
private CropCanvasView cropCanvasView; private CropCanvasView cropCanvasView;
private ZoomContainerView zoomContainer;
private Bitmap originalBitmap; private Bitmap originalBitmap;
private String imagePath; private String imagePath;
private String albumPath; private String albumPath;
@@ -73,7 +87,66 @@ public class CropActivity extends AppCompatActivity {
} }
}); });
findViewById(R.id.btn_info).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showCropInfoDialog();
}
});
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 = findViewById(R.id.crop_canvas_view);
final View colorView = findViewById(R.id.color_view);
final GradientDrawable colorDrawable = (GradientDrawable) colorView.getBackground();
cropCanvasView.setOnBackgroundColorChangedListener(new CropCanvasView.OnBackgroundColorChangedListener() {
@Override
public void onBackgroundColorChanged(int color) {
colorDrawable.setColor(color);
}
});
cropCanvasView.setOnColorPickedListener(new CropCanvasView.OnColorPickedListener() {
@Override
public void onColorPicked(int color) {
int pickX = cropCanvasView.getLastPickImageX();
int pickY = cropCanvasView.getLastPickImageY();
colorDrawable.setColor(color);
Toast.makeText(CropActivity.this,
"颜色已拾取: #" + String.format("%06X", color & 0xFFFFFF) +
" (" + pickX + "," + pickY + ")",
Toast.LENGTH_SHORT).show();
}
});
cropCanvasView.setOnColorPickEndListener(new CropCanvasView.OnColorPickEndListener() {
@Override
public void onColorPickEnd() {
cropCanvasView.setColorPickMode(false);
btnColorPick.setAlpha(1.0f);
}
});
colorDrawable.setColor(cropCanvasView.getBackgroundColor());
loadImage(); loadImage();
} }
@@ -118,37 +191,60 @@ public class CropActivity extends AppCompatActivity {
} }
private void saveCroppedCover() { private void saveCroppedCover() {
Bitmap canvasBitmap = cropCanvasView.getCanvasBitmap(); if (originalBitmap == null || originalBitmap.isRecycled()) {
if (canvasBitmap == null) {
Toast.makeText(this, "Failed to get image", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Failed to get image", Toast.LENGTH_SHORT).show();
return; return;
} }
try { 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(); RectF cropRect = cropCanvasView.getCropRect();
int cropX = (int) cropRect.left; int bmpX = (int) cropRect.left;
int cropY = (int) cropRect.top; int bmpY = (int) cropRect.top;
int cropW = (int) cropRect.width(); int bmpW = (int) cropRect.width();
int cropH = (int) cropRect.height(); int bmpH = (int) cropRect.height();
cropX = Math.max(0, Math.min(cropX, canvasBitmap.getWidth() - 1)); bmpX = Math.max(0, Math.min(bmpX, canvasBitmap.getWidth() - 1));
cropY = Math.max(0, Math.min(cropY, canvasBitmap.getHeight() - 1)); bmpY = Math.max(0, Math.min(bmpY, canvasBitmap.getHeight() - 1));
cropW = Math.max(1, Math.min(cropW, canvasBitmap.getWidth() - cropX)); bmpW = Math.max(1, Math.min(bmpW, canvasBitmap.getWidth() - bmpX));
cropH = Math.max(1, Math.min(cropH, canvasBitmap.getHeight() - cropY)); bmpH = Math.max(1, Math.min(bmpH, canvasBitmap.getHeight() - bmpY));
Bitmap cropped = Bitmap.createBitmap(canvasBitmap, cropX, cropY, cropW, cropH); 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());
File cacheDir = getCacheDir();
File coverFile = new File(cacheDir, "cover_" + System.currentTimeMillis() + ".png");
FileOutputStream fos = new FileOutputStream(coverFile); FileOutputStream fos = new FileOutputStream(coverFile);
cropped.compress(Bitmap.CompressFormat.PNG, 100, fos); cropped.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.close(); fos.close();
LogUtils.d(TAG, "saveCroppedCover: file exists=" + coverFile.exists() + ", length=" + coverFile.length());
cropped.recycle(); cropped.recycle();
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this); AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
coverDbHelper.setCoverWithCrop(albumPath, imagePath, coverFile.getAbsolutePath()); 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(); Toast.makeText(this, "封面已保存", Toast.LENGTH_SHORT).show();
setResult(RESULT_OK); setResult(RESULT_OK);
@@ -159,6 +255,67 @@ public class CropActivity extends AppCompatActivity {
} }
} }
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");
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);
infoText.setText(info.toString());
if (previewBitmap != null) {
previewImage.setImageBitmap(previewBitmap);
}
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 @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();

View File

@@ -4,6 +4,7 @@ import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.RectF; import android.graphics.RectF;
import android.util.AttributeSet; import android.util.AttributeSet;
@@ -11,6 +12,33 @@ import android.view.MotionEvent;
import android.view.View; import android.view.View;
public class CropCanvasView extends 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 imagePaint;
private Paint borderPaint; private Paint borderPaint;
private Paint cornerPaint; private Paint cornerPaint;
@@ -39,11 +67,23 @@ public class CropCanvasView extends View {
private RectF canvasBounds = new RectF(); private RectF canvasBounds = new RectF();
private Bitmap originalBitmap; private Bitmap originalBitmap;
private Bitmap canvasBitmap; private Bitmap canvasBitmap;
private float bitmapScale = 1.0f;
private Bitmap displayBitmap;
private RectF initialSpanRect;
private float initialSpan; private float initialSpan;
private int backgroundColor = Color.GREEN; private int backgroundColor = Color.BLUE;
private boolean colorPickMode = false; private boolean colorPickMode = false;
private int previewColor = 0;
private float lastPickX = 0;
private float lastPickY = 0;
private float pickX, pickY; 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) { public CropCanvasView(Context context) {
super(context); super(context);
init(); init();
@@ -74,7 +114,81 @@ public class CropCanvasView extends View {
} }
public void setImageBitmap(Bitmap bitmap) { public void setImageBitmap(Bitmap bitmap) {
if (displayBitmap != null && displayBitmap != originalBitmap) {
displayBitmap.recycle();
}
this.originalBitmap = bitmap; 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);
canvas.drawColor(backgroundColor);
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) { public void initCanvas(int imgWidth, int imgHeight, float ratio) {
@@ -100,33 +214,66 @@ public class CropCanvasView extends View {
imageBounds.set(left, top, left + imageWidth, top + imageHeight); imageBounds.set(left, top, left + imageWidth, top + imageHeight);
canvasBounds.set(0, 0, canvasWidth, canvasHeight); canvasBounds.set(0, 0, canvasWidth, canvasHeight);
cropRect = new RectF(0, 0, canvasWidth, canvasHeight); cropRect = new RectF(canvasBounds);
createCanvasBitmap();
requestLayout(); requestLayout();
invalidate(); invalidate();
} }
private void createCanvasBitmap() { public void getDisplayMatrix(Matrix matrix) {
if (canvasBitmap != null) { if (canvasWidth <= 0 || canvasHeight <= 0 || getWidth() <= 0 || getHeight() <= 0) {
canvasBitmap.recycle(); return;
}
canvasBitmap = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888);
android.graphics.Canvas canvas = new android.graphics.Canvas(canvasBitmap);
canvas.drawColor(backgroundColor);
if (originalBitmap != null && !originalBitmap.isRecycled()) {
canvas.drawBitmap(originalBitmap, imageBounds.left, imageBounds.top, imagePaint);
} }
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 Bitmap getCanvasBitmap() { public float screenToImageX(float screenX) {
return canvasBitmap; 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) { public void setBackgroundColor(int color) {
this.backgroundColor = color; this.backgroundColor = color;
createCanvasBitmap(); if (backgroundColorChangedListener != null) {
backgroundColorChangedListener.onBackgroundColorChanged(color);
}
invalidate(); invalidate();
} }
@@ -134,6 +281,40 @@ public class CropCanvasView extends View {
return backgroundColor; 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) { public void setColorPickMode(boolean enable) {
this.colorPickMode = enable; this.colorPickMode = enable;
if (enable) { if (enable) {
@@ -194,6 +375,14 @@ public class CropCanvasView extends View {
return new RectF(imageBounds); return new RectF(imageBounds);
} }
public float getBitmapScale() {
return displayScale;
}
public float getDisplayScale() {
return displayScale;
}
@Override @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int w = canvasWidth > 0 ? canvasWidth : 0; int w = canvasWidth > 0 ? canvasWidth : 0;
@@ -201,21 +390,75 @@ public class CropCanvasView extends View {
setMeasuredDimension(w, h); 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 @Override
protected void onDraw(Canvas canvas) { protected void onDraw(Canvas canvas) {
super.onDraw(canvas); super.onDraw(canvas);
if (canvasBitmap != null && !canvasBitmap.isRecycled()) { if (cropRect == null) return;
canvas.drawBitmap(canvasBitmap, 0, 0, null);
}
if (cropRect != null) { if (displayBitmap != null && !displayBitmap.isRecycled()) {
canvas.drawRect(cropRect, borderPaint); canvas.save();
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint); Matrix matrix = new Matrix();
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint); getDisplayMatrix(matrix);
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint); canvas.concat(matrix);
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
canvas.drawColor(backgroundColor);
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);
}
} }
} }
@@ -240,8 +483,15 @@ public class CropCanvasView extends View {
newWidth = Math.max(minSize, Math.min(newWidth, canvasBounds.width())); newWidth = Math.max(minSize, Math.min(newWidth, canvasBounds.width()));
newHeight = newWidth / coverRatio; newHeight = newWidth / coverRatio;
cropRect.set(centerX - newWidth / 2, centerY - newHeight / 2, float newLeft = centerX - newWidth / 2;
centerX + newWidth / 2, centerY + newHeight / 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; initialSpan = span;
invalidate(); invalidate();
@@ -253,12 +503,59 @@ public class CropCanvasView extends View {
float x = event.getX(); float x = event.getX();
float y = event.getY(); 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 (colorPickMode) {
if (event.getAction() == MotionEvent.ACTION_DOWN) { if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) {
pickX = x; if (originalBitmap != null && !originalBitmap.isRecycled()) {
pickY = y; float imgX = screenToImageX(x);
int color = getColorAt(x, y); float imgY = screenToImageY(y);
setBackgroundColor(color); if (imageBounds.contains(imgX, imgY)) {
previewColor = getImageColorAt(x, y);
} else if (canvasBounds.contains(x, y)) {
previewColor = backgroundColor;
} 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 = backgroundColor;
}
if (colorPickedListener != null) {
colorPickedListener.onColorPicked(pickedColor);
}
}
if (colorPickEndListener != null) {
colorPickEndListener.onColorPickEnd();
}
previewColor = 0;
invalidate();
return true; return true;
} }
return true; return true;
@@ -281,11 +578,11 @@ public class CropCanvasView extends View {
float newRight = cropRect.right + dx; float newRight = cropRect.right + dx;
float newBottom = cropRect.bottom + dy; float newBottom = cropRect.bottom + dy;
if (newLeft >= canvasBounds.left && newRight <= canvasBounds.right) { if (newLeft >= bounds.left && newRight <= bounds.right) {
cropRect.left = newLeft; cropRect.left = newLeft;
cropRect.right = newRight; cropRect.right = newRight;
} }
if (newTop >= canvasBounds.top && newBottom <= canvasBounds.bottom) { if (newTop >= bounds.top && newBottom <= bounds.bottom) {
cropRect.top = newTop; cropRect.top = newTop;
cropRect.bottom = newBottom; cropRect.bottom = newBottom;
} }
@@ -387,14 +684,6 @@ public class CropCanvasView extends View {
} }
public int getColorAt(float x, float y) { public int getColorAt(float x, float y) {
if (canvasBitmap != null && !canvasBitmap.isRecycled()) { return getImageColorAt(x, y);
int bitmapX = (int) x;
int bitmapY = (int) y;
if (bitmapX >= 0 && bitmapX < canvasBitmap.getWidth() &&
bitmapY >= 0 && bitmapY < canvasBitmap.getHeight()) {
return canvasBitmap.getPixel(bitmapX, bitmapY);
}
}
return Color.TRANSPARENT;
} }
} }

View File

@@ -1,6 +1,7 @@
package cc.winboll.studio.gallery; package cc.winboll.studio.gallery;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -94,6 +95,14 @@ public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder>
this.albumPath = 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() { public void refreshBg() {
if (prefs != null) { if (prefs != null) {
bgType = prefs.getBgType(); bgType = prefs.getBgType();
@@ -111,16 +120,38 @@ public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder>
private void showContextMenu(View view, final int position) { private void showContextMenu(View view, final int position) {
final String imagePath = imagePaths.get(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()); android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext());
builder.setTitle("Image"); builder.setTitle("Image");
boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath);
String[] items = isPinned ? new String[]{"取消置顶", "设置为封面"} : new String[]{"置顶", "设置为封面"}; 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() { builder.setItems(items, new android.content.DialogInterface.OnClickListener() {
@Override @Override
public void onClick(android.content.DialogInterface dialog, int which) { public void onClick(android.content.DialogInterface dialog, int which) {
if (which == 0) { if (which == 0) {
if (pinnedDbHelper != null) { if (pinnedDbHelper != null) {
if (isPinned) { if (isPinned[0]) {
pinnedDbHelper.unpinImage(imagePath); pinnedDbHelper.unpinImage(imagePath);
} else { } else {
pinnedDbHelper.pinImage(imagePath); pinnedDbHelper.pinImage(imagePath);
@@ -129,7 +160,17 @@ public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder>
} }
} else if (which == 1) { } else if (which == 1) {
if (coverDbHelper != null && albumPath != null) { if (coverDbHelper != null && albumPath != null) {
coverDbHelper.setCover(albumPath, imagePath); 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(); notifyDataSetChanged();
} }
} }
@@ -162,7 +203,11 @@ public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder>
boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath); boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath);
holder.pinIcon.setVisibility(isPinned ? View.VISIBLE : View.GONE); holder.pinIcon.setVisibility(isPinned ? View.VISIBLE : View.GONE);
boolean isCover = coverDbHelper != null && albumPath != null && imagePath.equals(coverDbHelper.getCover(albumPath)); 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.coverIcon.setVisibility(isCover ? View.VISIBLE : View.GONE);
holder.itemView.setOnClickListener(new OnClickListener() { holder.itemView.setOnClickListener(new OnClickListener() {

View File

@@ -1,8 +1,11 @@
package cc.winboll.studio.gallery; package cc.winboll.studio.gallery;
import android.Manifest; import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.database.Cursor; import android.database.Cursor;
import android.media.MediaScannerConnection; import android.media.MediaScannerConnection;
@@ -192,9 +195,17 @@ private void loadAlbums() {
LogUtils.d(TAG, "scanning folder: " + subfolder.getName()); LogUtils.d(TAG, "scanning folder: " + subfolder.getName());
String albumPath = subfolder.getAbsolutePath(); String albumPath = subfolder.getAbsolutePath();
String coverPath = coverDbHelper.getCover(albumPath); String coverPath = coverDbHelper.getCover(albumPath);
LogUtils.d(TAG, "loadAlbums: album=" + albumPath + ", coverPath=" + coverPath);
Uri coverUri = null; Uri coverUri = null;
if (coverPath != null) { if (coverPath != null) {
coverUri = getUriFromPath(coverPath); 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) { if (coverUri == null) {
ArrayList<Uri> images = getImagesInFolder(albumPath); ArrayList<Uri> images = getImagesInFolder(albumPath);
@@ -281,26 +292,32 @@ private void loadAlbums() {
if (id == R.id.action_settings) { if (id == R.id.action_settings) {
startActivity(new Intent(this, SettingsActivity.class)); startActivity(new Intent(this, SettingsActivity.class));
return true; return true;
} else if (id == R.id.action_about) {
startActivity(new Intent(this, AboutActivity.class));
return true;
} else if (id == R.id.action_trash) { } else if (id == R.id.action_trash) {
startActivity(new Intent(this, TrashActivity.class)); startActivity(new Intent(this, TrashActivity.class));
return true; return true;
} else if (id == R.id.action_refresh) {
if (checkPermission()) {
loadAlbums();
}
return true;
} else if (id == R.id.action_debug) { } else if (id == R.id.action_debug) {
LogActivity.startLogActivity(this); LogActivity.startLogActivity(this);
// Log.d("Gallery", "Debug log message");
// Toast.makeText(this, R.string.debug_message, Toast.LENGTH_SHORT).show();
return true; return true;
} }
return super.onOptionsItemSelected(item); 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 @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
registerReceiver(coverUpdatedReceiver, new IntentFilter(Preferences.ACTION_COVER_UPDATED));
if (checkPermission()) { if (checkPermission()) {
scanMediaStore(); scanMediaStore();
loadAlbums(); loadAlbums();
@@ -308,9 +325,16 @@ private void loadAlbums() {
if (adapter != null) { if (adapter != null) {
adapter.refreshBg(); adapter.refreshBg();
adapter.refreshPinned(); adapter.refreshPinned();
adapter.refreshCover();
} }
} }
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(coverUpdatedReceiver);
}
private void scanMediaStore() { private void scanMediaStore() {
String folderPath = prefs.getFolderPath(); String folderPath = prefs.getFolderPath();
File baseFolder = new File(folderPath); File baseFolder = new File(folderPath);

View File

@@ -10,8 +10,17 @@ public class Preferences {
private static final String KEY_FOLDER_PATH = "folder_path"; private static final String KEY_FOLDER_PATH = "folder_path";
private static final String KEY_BG_TYPE = "bg_type"; 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_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_BG_TYPE = 0;
private static final int DEFAULT_SORT_MODE = 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"; 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_DESC = 0;
@@ -57,4 +66,28 @@ public class Preferences {
LogUtils.d(TAG, "setAlbumSortMode: " + mode); LogUtils.d(TAG, "setAlbumSortMode: " + mode);
prefs.edit().putInt(KEY_ALBUM_SORT_MODE, mode).apply(); 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();
}
} }

View File

@@ -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);
}
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@android:color/transparent"/>
<stroke android:width="2dp" android:color="@android:color/white"/>
<size android:width="32dp" android:height="32dp"/>
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke android:width="2dp" android:color="@android:color/white"/>
<size android:width="36dp" android:height="36dp"/>
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/black"/>
<stroke android:width="2dp" android:color="@android:color/white"/>
</shape>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,13H5v-2h14v2z"/>
</vector>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</com.google.android.material.appbar.AppBarLayout>
<cc.winboll.studio.libappbase.views.AboutView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/aboutview"/>
</LinearLayout>
</FrameLayout>

View File

@@ -21,6 +21,22 @@
android:src="@drawable/ic_close" android:src="@drawable/ic_close"
android:background="?attr/selectableItemBackgroundBorderless"/> android:background="?attr/selectableItemBackgroundBorderless"/>
<FrameLayout
android:id="@+id/color_icon_container"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="8dp"
android:background="@android:color/transparent">
<View
android:id="@+id/color_view"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
android:background="@drawable/bg_color_circle"/>
</FrameLayout>
<ImageView <ImageView
android:id="@+id/btn_color_pick" android:id="@+id/btn_color_pick"
android:layout_width="48dp" android:layout_width="48dp"
@@ -34,23 +50,82 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1"/> android:layout_weight="1"/>
<TextView <ImageView
android:id="@+id/btn_done" android:id="@+id/btn_info"
android:layout_width="wrap_content" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:text="完成" android:padding="12dp"
android:textColor="@android:color/white" android:src="@drawable/ic_info"
android:textSize="16sp" android:background="?attr/selectableItemBackgroundBorderless"/>
android:gravity="center"
android:paddingHorizontal="16dp" <ImageView
android:id="@+id/btn_done"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="12dp"
android:src="@drawable/ic_done"
android:background="?attr/selectableItemBackgroundBorderless"/> android:background="?attr/selectableItemBackgroundBorderless"/>
</LinearLayout> </LinearLayout>
<cc.winboll.studio.gallery.CropCanvasView <ScrollView
android:id="@+id/crop_canvas_view" android:id="@+id/scroll_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center"/> android:layout_marginTop="56dp"
android:layout_marginBottom="56dp"
android:fillViewport="true">
<cc.winboll.studio.gallery.ZoomContainerView
android:id="@+id/zoom_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center">
<cc.winboll.studio.gallery.CropCanvasView
android:id="@+id/crop_canvas_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
</cc.winboll.studio.gallery.ZoomContainerView>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_gravity="bottom"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="@color/colorPrimary"
android:paddingHorizontal="16dp"
android:layout_marginBottom="0dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="缩小"
android:textColor="@android:color/white"
android:textSize="12sp"/>
<SeekBar
android:id="@+id/seekbar_zoom"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:min="0"
android:max="100"
android:progress="50"
android:layout_marginHorizontal="8dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="放大"
android:textColor="@android:color/white"
android:textSize="12sp"/>
</LinearLayout>
</FrameLayout> </FrameLayout>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="@drawable/bg_dialog">
<TextView
android:id="@+id/info_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:fontFamily="monospace"
android:textColor="@android:color/white"/>
<ImageView
android:id="@+id/preview_image"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginTop="16dp"
android:scaleType="fitCenter"
android:background="@android:color/black"/>
</LinearLayout>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="40dp"
android:layout_height="40dp">
<View
android:id="@+id/color_view"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
android:background="@drawable/bg_color_circle"/>
<View
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center"
android:background="@drawable/bg_color_circle_border"/>
</FrameLayout>

View File

@@ -13,8 +13,8 @@
app:showAsAction="never"/> app:showAsAction="never"/>
<item <item
android:id="@+id/action_refresh" android:id="@+id/action_about"
android:title="@string/refresh" android:title="@string/about"
app:showAsAction="never"/> app:showAsAction="never"/>
<item <item

View File

@@ -1,8 +1,10 @@
<resources> <resources>
<string name="app_name">Gallery</string> <string name="app_name">Gallery</string>
<string name="app_description">WinBoLL Album App</string>
<string name="refresh">Refresh</string> <string name="refresh">Refresh</string>
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="settings_title">Settings</string> <string name="settings_title">Settings</string>
<string name="about">About</string>
<string name="folder_path">Folder Path</string> <string name="folder_path">Folder Path</string>
<string name="enter_folder_path">Enter folder path</string> <string name="enter_folder_path">Enter folder path</string>
<string name="save">Save</string> <string name="save">Save</string>
@@ -16,4 +18,4 @@
<string name="no">No</string> <string name="no">No</string>
<string name="debug_log">Debug Log</string> <string name="debug_log">Debug Log</string>
<string name="debug_message">Debug log message</string> <string name="debug_message">Debug log message</string>
</resources> </resources>