Compare commits
26 Commits
gallery
...
positions-
| Author | SHA1 | Date | |
|---|---|---|---|
| c509a0126c | |||
| 07a7409d10 | |||
| f1a7313ac1 | |||
| 41492a2251 | |||
| b5431ccac2 | |||
| ee3e202ecf | |||
| 5e7828cf2b | |||
| 4c5df10c54 | |||
| c0084cd160 | |||
| f89dbede9c | |||
| c946d7af3a | |||
| b8ddd87e66 | |||
| 5ffc049790 | |||
| 0fcf1c5952 | |||
| 6d907e46cb | |||
| 5ab16c2387 | |||
| 160614ce2a | |||
| 1e0a9d222c | |||
| a93cad67a4 | |||
| db264eb85a | |||
| 8361cb0728 | |||
| 92f94f462f | |||
| 22c719d87c | |||
| 94483067cb | |||
| f21b69c64c | |||
| 30123efd4e |
@@ -1,8 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<!-- Put flavor specific strings here -->
|
|
||||||
<string name="app_name">Gallery+</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
|
||||||
<manifest
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="cc.winboll.studio.gallery">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/AppTheme"
|
|
||||||
android:resizeableActivity="true"
|
|
||||||
android:name=".GlobalWinBoLLApplication"
|
|
||||||
android:requestLegacyExternalStorage="true">
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:label="@string/app_name">
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
|
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
|
||||||
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".SettingsActivity"
|
|
||||||
android:label="@string/settings_title"/>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".AlbumActivity"
|
|
||||||
android:label="@string/app_name"/>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".ImageViewerActivity"
|
|
||||||
android:label="@string/app_name"/>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".TrashActivity"
|
|
||||||
android:label="@string/trash"/>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".CropActivity"
|
|
||||||
android:label="调整封面"/>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".AboutActivity"
|
|
||||||
android:label="@string/about"/>
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.max_aspect"
|
|
||||||
android:value="4.0"/>
|
|
||||||
|
|
||||||
<activity android:name=".GlobalApplication$CrashActivity"/>
|
|
||||||
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
package cc.winboll.studio.gallery;
|
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import androidx.core.app.ActivityCompat;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
import cc.winboll.studio.gallery.ImageAdapter.OnImageClickListener;
|
|
||||||
|
|
||||||
public class AlbumActivity extends AppCompatActivity {
|
|
||||||
public static final String TAG = "AlbumActivity";
|
|
||||||
private static final int PERMISSION_REQUEST_CODE = 101;
|
|
||||||
public static final String EXTRA_ALBUM_PATH = "album_path";
|
|
||||||
public static final String EXTRA_ALBUM_NAME = "album_name";
|
|
||||||
private RecyclerView recyclerView;
|
|
||||||
private ImageAdapter adapter;
|
|
||||||
private String albumPath;
|
|
||||||
private String albumName;
|
|
||||||
private FloatingActionButton fabScrollTop;
|
|
||||||
private Preferences prefs;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_main);
|
|
||||||
LogUtils.d(TAG, "onCreate");
|
|
||||||
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
|
|
||||||
albumPath = getIntent().getStringExtra(EXTRA_ALBUM_PATH);
|
|
||||||
albumName = getIntent().getStringExtra(EXTRA_ALBUM_NAME);
|
|
||||||
|
|
||||||
getSupportActionBar().setTitle(albumName);
|
|
||||||
|
|
||||||
prefs = new Preferences(this);
|
|
||||||
|
|
||||||
recyclerView = findViewById(R.id.recycler_view);
|
|
||||||
recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
|
|
||||||
adapter = new ImageAdapter();
|
|
||||||
adapter.setContext(this);
|
|
||||||
adapter.setAlbumPath(albumPath);
|
|
||||||
recyclerView.setAdapter(adapter);
|
|
||||||
|
|
||||||
fabScrollTop = findViewById(R.id.fab_scroll_top);
|
|
||||||
fabScrollTop.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
recyclerView.scrollToPosition(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
|
||||||
@Override
|
|
||||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
|
||||||
super.onScrolled(recyclerView, dx, dy);
|
|
||||||
GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
|
|
||||||
if (layoutManager != null) {
|
|
||||||
int firstVisible = layoutManager.findFirstVisibleItemPosition();
|
|
||||||
if (firstVisible > 0) {
|
|
||||||
fabScrollTop.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
fabScrollTop.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
adapter.setOnImageClickListener(new OnImageClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onImageClick(int position, ArrayList<Uri> urls, ArrayList<String> paths) {
|
|
||||||
Intent intent = new Intent(AlbumActivity.this, ImageViewerActivity.class);
|
|
||||||
intent.putParcelableArrayListExtra(ImageViewerActivity.EXTRA_IMAGE_URLS, urls);
|
|
||||||
intent.putStringArrayListExtra(ImageViewerActivity.EXTRA_POSITIONS, paths);
|
|
||||||
intent.putExtra(ImageViewerActivity.EXTRA_POSITION, position);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (checkPermission()) {
|
|
||||||
loadImages();
|
|
||||||
} else {
|
|
||||||
requestPermission();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean checkPermission() {
|
|
||||||
return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
|
|
||||||
== PackageManager.PERMISSION_GRANTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void requestPermission() {
|
|
||||||
ActivityCompat.requestPermissions(this,
|
|
||||||
new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE},
|
|
||||||
PERMISSION_REQUEST_CODE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
|
||||||
@NonNull int[] grantResults) {
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
||||||
if (requestCode == PERMISSION_REQUEST_CODE) {
|
|
||||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
loadImages();
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getSortOrder(int sortMode) {
|
|
||||||
switch (sortMode) {
|
|
||||||
case Preferences.SORT_TIME_ASC:
|
|
||||||
return android.provider.MediaStore.Images.Media.DATE_ADDED + " ASC";
|
|
||||||
case Preferences.SORT_NAME_DESC:
|
|
||||||
return android.provider.MediaStore.Images.Media.DISPLAY_NAME + " DESC";
|
|
||||||
case Preferences.SORT_NAME_ASC:
|
|
||||||
return android.provider.MediaStore.Images.Media.DISPLAY_NAME + " ASC";
|
|
||||||
case Preferences.SORT_TIME_DESC:
|
|
||||||
default:
|
|
||||||
return android.provider.MediaStore.Images.Media.DATE_ADDED + " DESC";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadImages() {
|
|
||||||
LogUtils.d(TAG, "loadImages");
|
|
||||||
ArrayList<Uri> imageUrls = new ArrayList<>();
|
|
||||||
ArrayList<String> imagePaths = new ArrayList<>();
|
|
||||||
ContentResolver contentResolver = getContentResolver();
|
|
||||||
Uri collection = android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
|
||||||
|
|
||||||
String selection = android.provider.MediaStore.Images.Media.DATA + " LIKE ?";
|
|
||||||
String[] selectionArgs = new String[]{albumPath + "%"};
|
|
||||||
int sortMode = prefs.getAlbumSortMode();
|
|
||||||
String sortOrder = getSortOrder(sortMode);
|
|
||||||
|
|
||||||
try (Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder)) {
|
|
||||||
if (cursor != null) {
|
|
||||||
int dataColumn = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.DATA);
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
String path = cursor.getString(dataColumn);
|
|
||||||
if (path != null && path.startsWith(albumPath + "/")) {
|
|
||||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media._ID));
|
|
||||||
Uri contentUri = Uri.withAppendedPath(collection, String.valueOf(id));
|
|
||||||
imageUrls.add(contentUri);
|
|
||||||
imagePaths.add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageUrls.isEmpty()) {
|
|
||||||
Toast.makeText(this, R.string.no_images_found, Toast.LENGTH_SHORT).show();
|
|
||||||
LogUtils.i(TAG, "No images found");
|
|
||||||
}
|
|
||||||
adapter.setData(imageUrls, imagePaths);
|
|
||||||
LogUtils.d(TAG, "Loaded " + imageUrls.size() + " images");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
getMenuInflater().inflate(R.menu.menu_album, menu);
|
|
||||||
int sortMode = prefs.getAlbumSortMode();
|
|
||||||
int menuId = getSortMenuId(sortMode);
|
|
||||||
MenuItem item = menu.findItem(menuId);
|
|
||||||
if (item != null) {
|
|
||||||
item.setChecked(true);
|
|
||||||
}
|
|
||||||
MenuItem sortItem = menu.findItem(R.id.action_sort);
|
|
||||||
if (sortItem != null && sortItem.getSubMenu() != null) {
|
|
||||||
sortItem.getSubMenu().setGroupCheckable(0, true, true);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getSortMenuId(int sortMode) {
|
|
||||||
switch (sortMode) {
|
|
||||||
case Preferences.SORT_TIME_ASC:
|
|
||||||
return R.id.sort_time_asc;
|
|
||||||
case Preferences.SORT_NAME_DESC:
|
|
||||||
return R.id.sort_name_desc;
|
|
||||||
case Preferences.SORT_NAME_ASC:
|
|
||||||
return R.id.sort_name_asc;
|
|
||||||
case Preferences.SORT_TIME_DESC:
|
|
||||||
default:
|
|
||||||
return R.id.sort_time_desc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
|
||||||
int itemId = item.getItemId();
|
|
||||||
if (itemId == R.id.sort_time_desc) {
|
|
||||||
prefs.setAlbumSortMode(Preferences.SORT_TIME_DESC);
|
|
||||||
item.setChecked(true);
|
|
||||||
loadImages();
|
|
||||||
return true;
|
|
||||||
} else if (itemId == R.id.sort_time_asc) {
|
|
||||||
prefs.setAlbumSortMode(Preferences.SORT_TIME_ASC);
|
|
||||||
item.setChecked(true);
|
|
||||||
loadImages();
|
|
||||||
return true;
|
|
||||||
} else if (itemId == R.id.sort_name_desc) {
|
|
||||||
prefs.setAlbumSortMode(Preferences.SORT_NAME_DESC);
|
|
||||||
item.setChecked(true);
|
|
||||||
loadImages();
|
|
||||||
return true;
|
|
||||||
} else if (itemId == R.id.sort_name_asc) {
|
|
||||||
prefs.setAlbumSortMode(Preferences.SORT_NAME_ASC);
|
|
||||||
item.setChecked(true);
|
|
||||||
loadImages();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
if (checkPermission()) {
|
|
||||||
loadImages();
|
|
||||||
}
|
|
||||||
if (adapter != null) {
|
|
||||||
adapter.refreshBg();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
|
||||||
if (requestCode == 100 && resultCode == RESULT_OK) {
|
|
||||||
loadImages();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
package cc.winboll.studio.gallery;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.provider.MediaStore;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.view.View.OnLongClickListener;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import com.bumptech.glide.Glide;
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
|
|
||||||
public class AlbumAdapter extends RecyclerView.Adapter<AlbumAdapter.ViewHolder> {
|
|
||||||
public static final String TAG = "AlbumAdapter";
|
|
||||||
private ArrayList<Album> albums = new ArrayList<>();
|
|
||||||
private OnAlbumClickListener listener;
|
|
||||||
private Preferences prefs;
|
|
||||||
private int bgType = 0;
|
|
||||||
private PinnedAlbumDbHelper pinnedDbHelper;
|
|
||||||
private OnCoverSizeListener coverSizeListener;
|
|
||||||
private boolean coverSizeReported = false;
|
|
||||||
|
|
||||||
private int getBgRes() {
|
|
||||||
switch (bgType) {
|
|
||||||
case 0:
|
|
||||||
return R.drawable.bg_checkerboard;
|
|
||||||
case 1:
|
|
||||||
return R.drawable.bg_white;
|
|
||||||
case 2:
|
|
||||||
return R.drawable.bg_black;
|
|
||||||
default:
|
|
||||||
return R.drawable.bg_checkerboard;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnAlbumClickListener {
|
|
||||||
void onAlbumClick(Album album);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnCoverSizeListener {
|
|
||||||
void onCoverSize(int width, int height);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final int DEFAULT_COVER_WIDTH = 240;
|
|
||||||
public static final int DEFAULT_COVER_HEIGHT = 120;
|
|
||||||
|
|
||||||
public void setOnAlbumClickListener(OnAlbumClickListener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnCoverSizeListener(OnCoverSizeListener listener) {
|
|
||||||
this.coverSizeListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setData(ArrayList<Album> albums) {
|
|
||||||
this.albums = sortAlbums(albums);
|
|
||||||
LogUtils.d(TAG, "setData: " + albums.size() + " albums");
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ArrayList<Album> sortAlbums(ArrayList<Album> list) {
|
|
||||||
if (pinnedDbHelper == null || list == null || list.isEmpty()) {
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
ArrayList<Album> pinned = new ArrayList<>();
|
|
||||||
ArrayList<Album> unpinned = new ArrayList<>();
|
|
||||||
for (Album album : list) {
|
|
||||||
if (pinnedDbHelper.isPinned(album.getPath())) {
|
|
||||||
pinned.add(album);
|
|
||||||
} else {
|
|
||||||
unpinned.add(album);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pinned.addAll(unpinned);
|
|
||||||
return pinned;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setContext(android.content.Context context) {
|
|
||||||
prefs = new Preferences(context);
|
|
||||||
bgType = prefs.getBgType();
|
|
||||||
pinnedDbHelper = PinnedAlbumDbHelper.getInstance(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refreshBg() {
|
|
||||||
if (prefs != null) {
|
|
||||||
bgType = prefs.getBgType();
|
|
||||||
}
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refreshCover() {
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refreshPinned() {
|
|
||||||
if (pinnedDbHelper != null && albums != null && !albums.isEmpty()) {
|
|
||||||
ArrayList<Album> pinned = new ArrayList<>();
|
|
||||||
ArrayList<Album> unpinned = new ArrayList<>();
|
|
||||||
for (Album album : albums) {
|
|
||||||
if (pinnedDbHelper.isPinned(album.getPath())) {
|
|
||||||
pinned.add(album);
|
|
||||||
} else {
|
|
||||||
unpinned.add(album);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Comparator<Album> nameComparator = new Comparator<Album>() {
|
|
||||||
@Override
|
|
||||||
public int compare(Album a1, Album a2) {
|
|
||||||
return a1.getName().compareToIgnoreCase(a2.getName());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Collections.sort(pinned, nameComparator);
|
|
||||||
Collections.sort(unpinned, nameComparator);
|
|
||||||
albums.clear();
|
|
||||||
albums.addAll(pinned);
|
|
||||||
albums.addAll(unpinned);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showContextMenu(View view, final Album album) {
|
|
||||||
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext());
|
|
||||||
builder.setTitle(album.getName());
|
|
||||||
final boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(album.getPath());
|
|
||||||
String[] items = isPinned ? new String[]{"取消置顶"} : new String[]{"置顶"};
|
|
||||||
builder.setItems(items, new android.content.DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(android.content.DialogInterface dialog, int which) {
|
|
||||||
if (pinnedDbHelper != null) {
|
|
||||||
if (which == 0) {
|
|
||||||
if (isPinned) {
|
|
||||||
pinnedDbHelper.unpinAlbum(album.getPath());
|
|
||||||
} else {
|
|
||||||
pinnedDbHelper.pinAlbum(album.getPath());
|
|
||||||
}
|
|
||||||
refreshPinned();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
builder.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_album, parent, false);
|
|
||||||
return new ViewHolder(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
|
|
||||||
final Album album = albums.get(position);
|
|
||||||
LogUtils.d(TAG, "bind: " + album.getName() + ", cover=" + album.getCoverUri());
|
|
||||||
|
|
||||||
holder.coverImage.setBackgroundResource(getBgRes());
|
|
||||||
|
|
||||||
boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(album.getPath());
|
|
||||||
holder.pinIcon.setVisibility(isPinned ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onLongClick(View v) {
|
|
||||||
showContextMenu(holder.itemView, album);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
holder.albumName.setText(album.getName());
|
|
||||||
holder.imageCount.setText(album.getImageCount() + " photos");
|
|
||||||
|
|
||||||
holder.itemView.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (!coverSizeReported && prefs != null) {
|
|
||||||
int savedWidth = prefs.getCoverWidth();
|
|
||||||
if (savedWidth == AlbumAdapter.DEFAULT_COVER_WIDTH) {
|
|
||||||
int width = holder.coverImage.getWidth();
|
|
||||||
int height = holder.coverImage.getHeight();
|
|
||||||
if (width > 0 && height > 0) {
|
|
||||||
prefs.setCoverWidth(width);
|
|
||||||
prefs.setCoverHeight(height);
|
|
||||||
float ratio = (float) width / height;
|
|
||||||
prefs.setCoverRatio(ratio);
|
|
||||||
coverSizeReported = true;
|
|
||||||
if (coverSizeListener != null) {
|
|
||||||
coverSizeListener.onCoverSize(width, height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
holder.itemView.setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
if (listener != null) {
|
|
||||||
listener.onAlbumClick(album);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Uri coverUri = album.getCoverUri();
|
|
||||||
if (coverUri != null) {
|
|
||||||
String uriString = coverUri.toString();
|
|
||||||
LogUtils.d(TAG, "uri scheme: " + coverUri.getScheme() + ", path: " + uriString);
|
|
||||||
|
|
||||||
if ("file".equals(coverUri.getScheme())) {
|
|
||||||
File coverFile = new File(coverUri.getPath());
|
|
||||||
if (coverFile.exists()) {
|
|
||||||
Glide.with(holder.coverImage.getContext())
|
|
||||||
.load(coverFile)
|
|
||||||
.fitCenter()
|
|
||||||
.into(holder.coverImage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Glide.with(holder.coverImage.getContext())
|
|
||||||
.load(coverUri)
|
|
||||||
.fitCenter()
|
|
||||||
.into(holder.coverImage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return albums.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
ImageView coverImage;
|
|
||||||
ImageView pinIcon;
|
|
||||||
TextView albumName;
|
|
||||||
TextView imageCount;
|
|
||||||
ViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
coverImage = itemView.findViewById(R.id.album_cover);
|
|
||||||
pinIcon = itemView.findViewById(R.id.pin_icon);
|
|
||||||
albumName = itemView.findViewById(R.id.album_name);
|
|
||||||
imageCount = itemView.findViewById(R.id.image_count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,750 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package cc.winboll.studio.gallery;
|
|
||||||
|
|
||||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
|
||||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
import cc.winboll.studio.libappbase.ToastUtils;
|
|
||||||
import cc.winboll.studio.gallery.utils.BackgroundUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
|
||||||
* @Date 2026/04/24 15:23
|
|
||||||
*/
|
|
||||||
public class GlobalWinBoLLApplication extends GlobalApplication {
|
|
||||||
|
|
||||||
public static final String TAG = "GlobalWinBoLLApplication";
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
LogUtils.d(TAG, "onCreate");
|
|
||||||
setIsDebugging(BuildConfig.DEBUG);
|
|
||||||
//setIsDebugging(false);
|
|
||||||
|
|
||||||
WinBoLLActivityManager.init(this);
|
|
||||||
|
|
||||||
BackgroundUtils.initFromPreferences(this);
|
|
||||||
|
|
||||||
// 初始化 Toast 框架
|
|
||||||
ToastUtils.init(this);
|
|
||||||
// 设置 Toast 布局样式
|
|
||||||
//ToastUtils.setView(R.layout.view_toast);
|
|
||||||
//ToastUtils.setStyle(new WhiteToastStyle());
|
|
||||||
//ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
|
|
||||||
|
|
||||||
//CrashHandler.getInstance().registerGlobal(this);
|
|
||||||
//CrashHandler.getInstance().registerPart(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTerminate() {
|
|
||||||
super.onTerminate();
|
|
||||||
LogUtils.d(TAG, "onTerminate");
|
|
||||||
ToastUtils.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
package cc.winboll.studio.gallery;
|
|
||||||
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import com.bumptech.glide.Glide;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
|
|
||||||
public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder> {
|
|
||||||
public static final String TAG = "ImageAdapter";
|
|
||||||
private ArrayList<Uri> imageUrls = new ArrayList<>();
|
|
||||||
private ArrayList<String> imagePaths = new ArrayList<>();
|
|
||||||
private OnImageClickListener listener;
|
|
||||||
private int bgType = 0;
|
|
||||||
private Preferences prefs;
|
|
||||||
private PinnedImageDbHelper pinnedDbHelper;
|
|
||||||
private AlbumCoverDbHelper coverDbHelper;
|
|
||||||
private String albumPath;
|
|
||||||
|
|
||||||
private int getBgRes() {
|
|
||||||
switch (bgType) {
|
|
||||||
case 0:
|
|
||||||
return R.drawable.bg_checkerboard;
|
|
||||||
case 1:
|
|
||||||
return R.drawable.bg_white;
|
|
||||||
case 2:
|
|
||||||
return R.drawable.bg_black;
|
|
||||||
default:
|
|
||||||
return R.drawable.bg_checkerboard;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnImageClickListener {
|
|
||||||
void onImageClick(int position, ArrayList<Uri> urls, ArrayList<String> paths);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnImageClickListener(OnImageClickListener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setData(ArrayList<Uri> urls, ArrayList<String> paths) {
|
|
||||||
this.imageUrls = urls;
|
|
||||||
this.imagePaths = paths;
|
|
||||||
sortPinnedFirst();
|
|
||||||
LogUtils.d(TAG, "setData: " + urls.size() + " images");
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sortPinnedFirst() {
|
|
||||||
if (pinnedDbHelper == null || imagePaths.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ArrayList<Uri> pinnedUrls = new ArrayList<>();
|
|
||||||
ArrayList<String> pinnedPaths = new ArrayList<>();
|
|
||||||
ArrayList<Uri> unpinnedUrls = new ArrayList<>();
|
|
||||||
ArrayList<String> unpinnedPaths = new ArrayList<>();
|
|
||||||
for (int i = 0; i < imagePaths.size(); i++) {
|
|
||||||
String path = imagePaths.get(i);
|
|
||||||
if (pinnedDbHelper.isPinned(path)) {
|
|
||||||
pinnedUrls.add(imageUrls.get(i));
|
|
||||||
pinnedPaths.add(path);
|
|
||||||
} else {
|
|
||||||
unpinnedUrls.add(imageUrls.get(i));
|
|
||||||
unpinnedPaths.add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
imageUrls.clear();
|
|
||||||
imagePaths.clear();
|
|
||||||
imageUrls.addAll(pinnedUrls);
|
|
||||||
imageUrls.addAll(unpinnedUrls);
|
|
||||||
imagePaths.addAll(pinnedPaths);
|
|
||||||
imagePaths.addAll(unpinnedPaths);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setContext(android.content.Context context) {
|
|
||||||
prefs = new Preferences(context);
|
|
||||||
bgType = prefs.getBgType();
|
|
||||||
pinnedDbHelper = PinnedImageDbHelper.getInstance(context);
|
|
||||||
coverDbHelper = AlbumCoverDbHelper.getInstance(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAlbumPath(String albumPath) {
|
|
||||||
this.albumPath = albumPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCropWidth() {
|
|
||||||
return prefs != null ? prefs.getCoverWidth() : 240;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCropHeight() {
|
|
||||||
return prefs != null ? prefs.getCoverHeight() : 120;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refreshBg() {
|
|
||||||
if (prefs != null) {
|
|
||||||
bgType = prefs.getBgType();
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refreshPinned() {
|
|
||||||
if (pinnedDbHelper == null || imagePaths.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sortPinnedFirst();
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showContextMenu(final View view, final int position) {
|
|
||||||
final String imagePath = imagePaths.get(position);
|
|
||||||
final Uri imageUri = imageUrls.get(position);
|
|
||||||
final boolean[] isPinned = {pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath)};
|
|
||||||
final boolean[] isCover = {false};
|
|
||||||
if (coverDbHelper != null && albumPath != null) {
|
|
||||||
String originalPath = coverDbHelper.getOriginalImagePath(albumPath);
|
|
||||||
isCover[0] = originalPath != null && originalPath.equals(imagePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext());
|
|
||||||
builder.setTitle("Image");
|
|
||||||
|
|
||||||
String[] items;
|
|
||||||
if (isPinned[0]) {
|
|
||||||
if (isCover[0]) {
|
|
||||||
items = new String[]{"取消置顶", "调整封面", "取消封面"};
|
|
||||||
} else {
|
|
||||||
items = new String[]{"取消置顶", "设置为封面"};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isCover[0]) {
|
|
||||||
items = new String[]{"置顶", "调整封面", "取消封面"};
|
|
||||||
} else {
|
|
||||||
items = new String[]{"置顶", "设置为封面"};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.setItems(items, new android.content.DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(android.content.DialogInterface dialog, int which) {
|
|
||||||
if (which == 0) {
|
|
||||||
if (pinnedDbHelper != null) {
|
|
||||||
if (isPinned[0]) {
|
|
||||||
pinnedDbHelper.unpinImage(imagePath);
|
|
||||||
} else {
|
|
||||||
pinnedDbHelper.pinImage(imagePath);
|
|
||||||
}
|
|
||||||
refreshPinned();
|
|
||||||
}
|
|
||||||
} else if (which == 1) {
|
|
||||||
if (coverDbHelper != null && albumPath != null) {
|
|
||||||
Intent cropIntent = new Intent(view.getContext(), CropActivity.class);
|
|
||||||
cropIntent.putExtra(CropActivity.EXTRA_IMAGE_URI, imageUri);
|
|
||||||
cropIntent.putExtra(CropActivity.EXTRA_IMAGE_PATH, imagePath);
|
|
||||||
cropIntent.putExtra(CropActivity.EXTRA_ALBUM_PATH, albumPath);
|
|
||||||
cropIntent.putExtra(CropActivity.EXTRA_CROP_WIDTH, getCropWidth());
|
|
||||||
cropIntent.putExtra(CropActivity.EXTRA_CROP_HEIGHT, getCropHeight());
|
|
||||||
((AlbumActivity) view.getContext()).startActivityForResult(cropIntent, 100);
|
|
||||||
}
|
|
||||||
} else if (which == 2) {
|
|
||||||
if (coverDbHelper != null && albumPath != null && isCover[0]) {
|
|
||||||
coverDbHelper.deleteCover(albumPath);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
builder.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_gallery, parent, false);
|
|
||||||
return new ViewHolder(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
|
|
||||||
holder.imageView.setBackgroundResource(getBgRes());
|
|
||||||
|
|
||||||
Glide.with(holder.imageView.getContext())
|
|
||||||
.load(imageUrls.get(position))
|
|
||||||
.centerCrop()
|
|
||||||
.into(holder.imageView);
|
|
||||||
|
|
||||||
final ArrayList<Uri> urls = imageUrls;
|
|
||||||
final ArrayList<String> paths = imagePaths;
|
|
||||||
final String imagePath = imagePaths.get(position);
|
|
||||||
|
|
||||||
boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath);
|
|
||||||
holder.pinIcon.setVisibility(isPinned ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
boolean isCover = false;
|
|
||||||
if (coverDbHelper != null && albumPath != null) {
|
|
||||||
String originalPath = coverDbHelper.getOriginalImagePath(albumPath);
|
|
||||||
isCover = originalPath != null && originalPath.equals(imagePath);
|
|
||||||
}
|
|
||||||
holder.coverIcon.setVisibility(isCover ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
holder.itemView.setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
if (listener != null) {
|
|
||||||
listener.onImageClick(position, urls, paths);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onLongClick(View v) {
|
|
||||||
showContextMenu(holder.itemView, position);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return imageUrls.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
ImageView imageView;
|
|
||||||
ImageView pinIcon;
|
|
||||||
ImageView coverIcon;
|
|
||||||
ViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
imageView = itemView.findViewById(R.id.image);
|
|
||||||
pinIcon = itemView.findViewById(R.id.pin_icon);
|
|
||||||
coverIcon = itemView.findViewById(R.id.cover_icon);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package cc.winboll.studio.gallery;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.viewpager.widget.PagerAdapter;
|
|
||||||
import com.bumptech.glide.Glide;
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
|
|
||||||
public class ImagePagerAdapter extends PagerAdapter {
|
|
||||||
public static final String TAG = "ImagePagerAdapter";
|
|
||||||
private ArrayList<Uri> imageUrls;
|
|
||||||
|
|
||||||
public ImagePagerAdapter(ArrayList<Uri> imageUrls) {
|
|
||||||
this.imageUrls = imageUrls;
|
|
||||||
LogUtils.d(TAG, "ImagePagerAdapter created with " + imageUrls.size() + " images");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCount() {
|
|
||||||
return imageUrls.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Object instantiateItem(@NonNull ViewGroup container, int position) {
|
|
||||||
View view = LayoutInflater.from(container.getContext())
|
|
||||||
.inflate(R.layout.item_image_pager, container, false);
|
|
||||||
view.setBackgroundResource(R.color.black);
|
|
||||||
ImageView imageView = view.findViewById(R.id.image);
|
|
||||||
|
|
||||||
Glide.with(imageView.getContext())
|
|
||||||
.load(imageUrls.get(position))
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.skipMemoryCache(true)
|
|
||||||
.into(imageView);
|
|
||||||
|
|
||||||
container.addView(view);
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
|
||||||
container.removeView((View) object);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
|
|
||||||
return view == object;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,397 +0,0 @@
|
|||||||
package cc.winboll.studio.gallery;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.provider.MediaStore;
|
|
||||||
import android.view.GestureDetector;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.OnTouchListener;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.viewpager.widget.ViewPager;
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
|
|
||||||
public class ImageViewerActivity extends Activity implements ViewPager.OnPageChangeListener {
|
|
||||||
public static final String TAG = "ImageViewerActivity";
|
|
||||||
public static final String EXTRA_IMAGE_URLS = "image_urls";
|
|
||||||
public static final String EXTRA_POSITIONS = "image_positions";
|
|
||||||
public static final String EXTRA_POSITION = "position";
|
|
||||||
|
|
||||||
private ArrayList<Uri> imageUrls;
|
|
||||||
private ArrayList<String> imagePaths;
|
|
||||||
private int currentPosition;
|
|
||||||
private ViewPager viewPager;
|
|
||||||
private View toolbar;
|
|
||||||
private ImageButton btnBack;
|
|
||||||
private ImageButton btnDelete;
|
|
||||||
private ImageButton btnShare;
|
|
||||||
private ImageButton btnInfo;
|
|
||||||
private ImageButton btnGallery;
|
|
||||||
private GestureDetector gestureDetector;
|
|
||||||
private TrashManager trashManager;
|
|
||||||
private Preferences prefs;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
|
||||||
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
|
||||||
setContentView(R.layout.activity_image_viewer);
|
|
||||||
LogUtils.d(TAG, "onCreate");
|
|
||||||
|
|
||||||
imageUrls = getIntent().getParcelableArrayListExtra(EXTRA_IMAGE_URLS);
|
|
||||||
imagePaths = getIntent().getStringArrayListExtra(EXTRA_POSITIONS);
|
|
||||||
currentPosition = getIntent().getIntExtra(EXTRA_POSITION, 0);
|
|
||||||
|
|
||||||
trashManager = new TrashManager(this);
|
|
||||||
prefs = new Preferences(this);
|
|
||||||
|
|
||||||
viewPager = findViewById(R.id.view_pager);
|
|
||||||
toolbar = findViewById(R.id.toolbar);
|
|
||||||
btnBack = findViewById(R.id.btn_back);
|
|
||||||
btnDelete = findViewById(R.id.btn_delete);
|
|
||||||
btnShare = findViewById(R.id.btn_share);
|
|
||||||
btnInfo = findViewById(R.id.btn_info);
|
|
||||||
|
|
||||||
btnGallery = findViewById(R.id.btn_gallery);
|
|
||||||
|
|
||||||
ImagePagerAdapter adapter = new ImagePagerAdapter(imageUrls);
|
|
||||||
viewPager.setAdapter(adapter);
|
|
||||||
viewPager.setCurrentItem(currentPosition);
|
|
||||||
viewPager.addOnPageChangeListener(this);
|
|
||||||
|
|
||||||
gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
|
||||||
toggleToolbar();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
viewPager.setOnTouchListener(new OnTouchListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onTouch(View v, MotionEvent event) {
|
|
||||||
return gestureDetector.onTouchEvent(event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
btnBack.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
btnDelete.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
showDeleteDialog();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
btnShare.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
shareCurrentImage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
btnInfo.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
showImageInfo();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
btnGallery.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
Toast.makeText(ImageViewerActivity.this, "Gallery", Toast.LENGTH_SHORT).show();
|
|
||||||
if (imageUrls != null && currentPosition >= 0 && currentPosition < imageUrls.size()) {
|
|
||||||
Uri imageUri = imageUrls.get(currentPosition);
|
|
||||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
|
||||||
intent.setDataAndType(imageUri, "image/*");
|
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
||||||
startActivity(Intent.createChooser(intent, "打开相册"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void toggleToolbar() {
|
|
||||||
if (toolbar.getVisibility() == View.VISIBLE) {
|
|
||||||
toolbar.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
toolbar.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showDeleteDialog() {
|
|
||||||
new AlertDialog.Builder(this)
|
|
||||||
.setMessage("Delete to trash?")
|
|
||||||
.setPositiveButton("Yes", new android.content.DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(android.content.DialogInterface dialog, int which) {
|
|
||||||
moveToTrash();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.setNegativeButton("No", null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void moveToTrash() {
|
|
||||||
LogUtils.d(TAG, "moveToTrash");
|
|
||||||
if (currentPosition >= 0 && currentPosition < imageUrls.size()) {
|
|
||||||
String imagePath = "";
|
|
||||||
if (imagePaths != null && currentPosition < imagePaths.size()) {
|
|
||||||
imagePath = imagePaths.get(currentPosition);
|
|
||||||
} else {
|
|
||||||
imagePath = getPathFromUri(imageUrls.get(currentPosition));
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri imageUri = imageUrls.get(currentPosition);
|
|
||||||
long result = -1;
|
|
||||||
|
|
||||||
if (!imagePath.isEmpty()) {
|
|
||||||
result = trashManager.addToTrash(imagePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result > 0) {
|
|
||||||
try {
|
|
||||||
getContentResolver().delete(imageUri, null, null);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
android.widget.Toast.makeText(this, "Moved to trash", android.widget.Toast.LENGTH_SHORT).show();
|
|
||||||
LogUtils.i(TAG, "Moved to trash");
|
|
||||||
removeCurrentImage();
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
int deleted = getContentResolver().delete(imageUri, null, null);
|
|
||||||
android.widget.Toast.makeText(this, "Deleted: " + deleted, android.widget.Toast.LENGTH_SHORT).show();
|
|
||||||
removeCurrentImage();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
removeCurrentImage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeCurrentImage() {
|
|
||||||
imageUrls.remove(currentPosition);
|
|
||||||
if (imagePaths != null) {
|
|
||||||
imagePaths.remove(currentPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageUrls.isEmpty()) {
|
|
||||||
finish();
|
|
||||||
} else {
|
|
||||||
if (currentPosition >= imageUrls.size()) {
|
|
||||||
currentPosition = imageUrls.size() - 1;
|
|
||||||
}
|
|
||||||
viewPager.setAdapter(new ImagePagerAdapter(imageUrls));
|
|
||||||
viewPager.setCurrentItem(currentPosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getPathFromUri(Uri uri) {
|
|
||||||
String[] projection = { MediaStore.Images.Media.DATA };
|
|
||||||
android.database.Cursor cursor = getContentResolver().query(uri, projection, null, null, null);
|
|
||||||
if (cursor != null) {
|
|
||||||
try {
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
|
|
||||||
return cursor.getString(columnIndex);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return uri.getPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void shareCurrentImage() {
|
|
||||||
if (currentPosition >= 0 && currentPosition < imageUrls.size()) {
|
|
||||||
Uri imageUri = imageUrls.get(currentPosition);
|
|
||||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
|
||||||
shareIntent.setType("image/*");
|
|
||||||
shareIntent.putExtra(Intent.EXTRA_STREAM, imageUri);
|
|
||||||
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
||||||
startActivity(Intent.createChooser(shareIntent, "Share Image"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showImageInfo() {
|
|
||||||
if (currentPosition < 0 || currentPosition >= imageUrls.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String imagePath = "";
|
|
||||||
if (imagePaths != null && currentPosition < imagePaths.size()) {
|
|
||||||
imagePath = imagePaths.get(currentPosition);
|
|
||||||
} else {
|
|
||||||
imagePath = getPathFromUri(imageUrls.get(currentPosition));
|
|
||||||
}
|
|
||||||
|
|
||||||
File imageFile = new File(imagePath);
|
|
||||||
if (!imageFile.exists()) {
|
|
||||||
imageFile = new File(imagePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
android.widget.LinearLayout layout = new android.widget.LinearLayout(this);
|
|
||||||
layout.setOrientation(android.widget.LinearLayout.VERTICAL);
|
|
||||||
layout.setPadding(48, 32, 48, 32);
|
|
||||||
|
|
||||||
android.widget.TextView labelPath = new android.widget.TextView(this);
|
|
||||||
labelPath.setText("Path:");
|
|
||||||
labelPath.setTextColor(getColor(android.R.color.darker_gray));
|
|
||||||
labelPath.setTextSize(14);
|
|
||||||
labelPath.setTypeface(null, android.graphics.Typeface.BOLD);
|
|
||||||
layout.addView(labelPath);
|
|
||||||
|
|
||||||
android.widget.TextView valuePath = new android.widget.TextView(this);
|
|
||||||
valuePath.setText(imagePath);
|
|
||||||
valuePath.setTextColor(getColor(android.R.color.black));
|
|
||||||
valuePath.setTextSize(14);
|
|
||||||
valuePath.setTextIsSelectable(true);
|
|
||||||
layout.addView(valuePath);
|
|
||||||
|
|
||||||
if (imageFile.exists()) {
|
|
||||||
long sizeBytes = imageFile.length();
|
|
||||||
String size;
|
|
||||||
if (sizeBytes < 1024) {
|
|
||||||
size = sizeBytes + " B";
|
|
||||||
} else if (sizeBytes < 1024 * 1024) {
|
|
||||||
size = String.format("%.2f KB", sizeBytes / 1024.0);
|
|
||||||
} else {
|
|
||||||
size = String.format("%.2f MB", sizeBytes / (1024.0 * 1024.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
android.widget.TextView labelSize = new android.widget.TextView(this);
|
|
||||||
labelSize.setText("Size:");
|
|
||||||
labelSize.setTextColor(getColor(android.R.color.darker_gray));
|
|
||||||
labelSize.setTextSize(14);
|
|
||||||
labelSize.setTypeface(null, android.graphics.Typeface.BOLD);
|
|
||||||
layout.addView(labelSize);
|
|
||||||
|
|
||||||
android.widget.TextView valueSize = new android.widget.TextView(this);
|
|
||||||
valueSize.setText(size);
|
|
||||||
valueSize.setTextColor(getColor(android.R.color.black));
|
|
||||||
valueSize.setTextSize(14);
|
|
||||||
valueSize.setTextIsSelectable(true);
|
|
||||||
layout.addView(valueSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
android.graphics.BitmapFactory.Options options = new android.graphics.BitmapFactory.Options();
|
|
||||||
options.inJustDecodeBounds = true;
|
|
||||||
android.graphics.BitmapFactory.decodeFile(imagePath, options);
|
|
||||||
if (options.outWidth > 0 && options.outHeight > 0) {
|
|
||||||
android.widget.TextView labelPixels = new android.widget.TextView(this);
|
|
||||||
labelPixels.setText("Pixels:");
|
|
||||||
labelPixels.setTextColor(getColor(android.R.color.darker_gray));
|
|
||||||
labelPixels.setTextSize(14);
|
|
||||||
labelPixels.setTypeface(null, android.graphics.Typeface.BOLD);
|
|
||||||
layout.addView(labelPixels);
|
|
||||||
|
|
||||||
android.widget.TextView valuePixels = new android.widget.TextView(this);
|
|
||||||
valuePixels.setText(options.outWidth + " x " + options.outHeight);
|
|
||||||
valuePixels.setTextColor(getColor(android.R.color.black));
|
|
||||||
valuePixels.setTextSize(14);
|
|
||||||
valuePixels.setTextIsSelectable(true);
|
|
||||||
layout.addView(valuePixels);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogUtils.e(TAG, "get pixels error: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
String[] projection = {
|
|
||||||
MediaStore.Images.Media.DATE_ADDED,
|
|
||||||
MediaStore.Images.Media.DATE_MODIFIED,
|
|
||||||
MediaStore.Images.Media.DATE_TAKEN
|
|
||||||
};
|
|
||||||
android.database.Cursor cursor = getContentResolver().query(
|
|
||||||
imageUrls.get(currentPosition), projection, null, null, null);
|
|
||||||
if (cursor != null) {
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
int dateAddedCol = cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED);
|
|
||||||
int dateTakenCol = cursor.getColumnIndex(MediaStore.Images.Media.DATE_TAKEN);
|
|
||||||
|
|
||||||
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
|
||||||
|
|
||||||
if (dateTakenCol >= 0) {
|
|
||||||
long dateTaken = cursor.getLong(dateTakenCol);
|
|
||||||
if (dateTaken > 0) {
|
|
||||||
android.widget.TextView labelTaken = new android.widget.TextView(this);
|
|
||||||
labelTaken.setText("Date Taken:");
|
|
||||||
labelTaken.setTextColor(getColor(android.R.color.darker_gray));
|
|
||||||
labelTaken.setTextSize(14);
|
|
||||||
labelTaken.setTypeface(null, android.graphics.Typeface.BOLD);
|
|
||||||
layout.addView(labelTaken);
|
|
||||||
|
|
||||||
android.widget.TextView valueTaken = new android.widget.TextView(this);
|
|
||||||
valueTaken.setText(sdf.format(new java.util.Date(dateTaken)));
|
|
||||||
valueTaken.setTextColor(getColor(android.R.color.black));
|
|
||||||
valueTaken.setTextSize(14);
|
|
||||||
valueTaken.setTextIsSelectable(true);
|
|
||||||
layout.addView(valueTaken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dateAddedCol >= 0) {
|
|
||||||
long dateAdded = cursor.getLong(dateAddedCol);
|
|
||||||
if (dateAdded > 0) {
|
|
||||||
android.widget.TextView labelAdded = new android.widget.TextView(this);
|
|
||||||
labelAdded.setText("Date Added:");
|
|
||||||
labelAdded.setTextColor(getColor(android.R.color.darker_gray));
|
|
||||||
labelAdded.setTextSize(14);
|
|
||||||
labelAdded.setTypeface(null, android.graphics.Typeface.BOLD);
|
|
||||||
layout.addView(labelAdded);
|
|
||||||
|
|
||||||
android.widget.TextView valueAdded = new android.widget.TextView(this);
|
|
||||||
valueAdded.setText(sdf.format(new java.util.Date(dateAdded * 1000)));
|
|
||||||
valueAdded.setTextColor(getColor(android.R.color.black));
|
|
||||||
valueAdded.setTextSize(14);
|
|
||||||
valueAdded.setTextIsSelectable(true);
|
|
||||||
layout.addView(valueAdded);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogUtils.e(TAG, "get date error: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
new AlertDialog.Builder(this)
|
|
||||||
.setTitle("Image Info")
|
|
||||||
.setView(layout)
|
|
||||||
.setPositiveButton("OK", null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPageSelected(int position) {
|
|
||||||
currentPosition = position;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPageScrollStateChanged(int state) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,419 +0,0 @@
|
|||||||
package cc.winboll.studio.gallery;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.media.MediaScannerConnection;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.provider.MediaStore;
|
|
||||||
import android.provider.Settings;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import androidx.core.app.ActivityCompat;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import cc.winboll.studio.gallery.AlbumAdapter.OnAlbumClickListener;
|
|
||||||
import cc.winboll.studio.gallery.utils.BackgroundUtils;
|
|
||||||
import cc.winboll.studio.libappbase.LogActivity;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
import com.a4455jkjh.colorpicker.ColorPickerDialog;
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileFilter;
|
|
||||||
import java.io.FilenameFilter;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
|
||||||
public static final String TAG = "MainActivity";
|
|
||||||
private static final int PERMISSION_REQUEST_CODE = 100;
|
|
||||||
private static final int MANAGE_PERMISSION_REQUEST_CODE = 101;
|
|
||||||
private RecyclerView recyclerView;
|
|
||||||
private AlbumAdapter adapter;
|
|
||||||
private Preferences prefs;
|
|
||||||
private FloatingActionButton fabScrollTop;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_main);
|
|
||||||
LogUtils.d(TAG, "onCreate");
|
|
||||||
|
|
||||||
View content = findViewById(android.R.id.content);
|
|
||||||
if (content != null) {
|
|
||||||
content.setBackground(BackgroundUtils.getInstance().getDrawable());
|
|
||||||
}
|
|
||||||
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
|
|
||||||
prefs = new Preferences(this);
|
|
||||||
|
|
||||||
recyclerView = findViewById(R.id.recycler_view);
|
|
||||||
recyclerView.setLayoutManager(new GridLayoutManager(this, 2));
|
|
||||||
adapter = new AlbumAdapter();
|
|
||||||
adapter.setContext(this);
|
|
||||||
recyclerView.setAdapter(adapter);
|
|
||||||
|
|
||||||
fabScrollTop = findViewById(R.id.fab_scroll_top);
|
|
||||||
fabScrollTop.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
recyclerView.scrollToPosition(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
|
||||||
@Override
|
|
||||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
|
||||||
super.onScrolled(recyclerView, dx, dy);
|
|
||||||
GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
|
|
||||||
if (layoutManager != null) {
|
|
||||||
int firstVisible = layoutManager.findFirstVisibleItemPosition();
|
|
||||||
if (firstVisible > 0) {
|
|
||||||
fabScrollTop.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
fabScrollTop.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
adapter.setOnAlbumClickListener(new OnAlbumClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onAlbumClick(Album album) {
|
|
||||||
Intent intent = new Intent(MainActivity.this, AlbumActivity.class);
|
|
||||||
intent.putExtra(AlbumActivity.EXTRA_ALBUM_PATH, album.getPath());
|
|
||||||
intent.putExtra(AlbumActivity.EXTRA_ALBUM_NAME, album.getName());
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
checkAndRequestPermissions();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkAndRequestPermissions() {
|
|
||||||
LogUtils.i(TAG, "checkAndRequestPermissions");
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
if (!Environment.isExternalStorageManager()) {
|
|
||||||
try {
|
|
||||||
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
|
|
||||||
intent.setData(Uri.parse("package:" + getPackageName()));
|
|
||||||
startActivityForResult(intent, MANAGE_PERMISSION_REQUEST_CODE);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
|
|
||||||
startActivityForResult(intent, MANAGE_PERMISSION_REQUEST_CODE);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkPermission()) {
|
|
||||||
loadAlbums();
|
|
||||||
} else {
|
|
||||||
requestPermission();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
|
||||||
if (requestCode == MANAGE_PERMISSION_REQUEST_CODE) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
if (Environment.isExternalStorageManager()) {
|
|
||||||
loadAlbums();
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this, "Permission required", Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean checkPermission() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
return Environment.isExternalStorageManager();
|
|
||||||
}
|
|
||||||
return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
|
||||||
== PackageManager.PERMISSION_GRANTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void requestPermission() {
|
|
||||||
ActivityCompat.requestPermissions(this,
|
|
||||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
|
|
||||||
PERMISSION_REQUEST_CODE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
|
||||||
@NonNull int[] grantResults) {
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
||||||
if (requestCode == PERMISSION_REQUEST_CODE) {
|
|
||||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
loadAlbums();
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadAlbums() {
|
|
||||||
LogUtils.d(TAG, "loadAlbums");
|
|
||||||
String folderPath = prefs.getFolderPath();
|
|
||||||
File baseFolder = new File(folderPath);
|
|
||||||
LogUtils.d(TAG, "baseFolder: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
|
|
||||||
|
|
||||||
if (!baseFolder.exists() || !baseFolder.isDirectory()) {
|
|
||||||
folderPath = Preferences.getDefaultPath();
|
|
||||||
baseFolder = new File(folderPath);
|
|
||||||
LogUtils.d(TAG, "try default: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
|
|
||||||
if (!baseFolder.exists()) {
|
|
||||||
folderPath = Environment.getExternalStorageDirectory() + "/Pictures";
|
|
||||||
baseFolder = new File(folderPath);
|
|
||||||
LogUtils.d(TAG, "try Pictures: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
|
|
||||||
ArrayList<Album> albums = new ArrayList<>();
|
|
||||||
|
|
||||||
FileFilter directoryFilter = new FileFilter() {
|
|
||||||
@Override
|
|
||||||
public boolean accept(File file) {
|
|
||||||
return file.isDirectory();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
File[] subfolders = baseFolder.listFiles(directoryFilter);
|
|
||||||
LogUtils.d(TAG, "subfolders: " + (subfolders != null ? subfolders.length : 0));
|
|
||||||
if (subfolders != null) {
|
|
||||||
for (File subfolder : subfolders) {
|
|
||||||
LogUtils.d(TAG, "scanning folder: " + subfolder.getName());
|
|
||||||
String albumPath = subfolder.getAbsolutePath();
|
|
||||||
String coverPath = coverDbHelper.getCover(albumPath);
|
|
||||||
LogUtils.d(TAG, "loadAlbums: album=" + albumPath + ", coverPath=" + coverPath);
|
|
||||||
Uri coverUri = null;
|
|
||||||
if (coverPath != null) {
|
|
||||||
File coverFile = new File(coverPath);
|
|
||||||
if (coverFile.exists()) {
|
|
||||||
coverUri = Uri.fromFile(coverFile);
|
|
||||||
LogUtils.d(TAG, "loadAlbums: cover from file=" + coverFile.getAbsolutePath());
|
|
||||||
} else {
|
|
||||||
coverUri = getUriFromPath(coverPath);
|
|
||||||
LogUtils.d(TAG, "loadAlbums: cover from media store path=" + coverPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (coverUri == null) {
|
|
||||||
ArrayList<Uri> images = getImagesInFolder(albumPath);
|
|
||||||
if (!images.isEmpty()) {
|
|
||||||
coverUri = images.get(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ArrayList<Uri> allImages = getImagesInFolder(albumPath);
|
|
||||||
if (coverUri != null || !allImages.isEmpty()) {
|
|
||||||
if (coverUri == null && !allImages.isEmpty()) {
|
|
||||||
coverUri = allImages.get(0);
|
|
||||||
}
|
|
||||||
int imageCount = allImages.size();
|
|
||||||
albums.add(new Album(subfolder.getName(), albumPath, coverUri, imageCount));
|
|
||||||
LogUtils.d(TAG, "album added: " + subfolder.getName() + ", " + imageCount + " images");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (albums.isEmpty()) {
|
|
||||||
Toast.makeText(this, R.string.no_images_found, Toast.LENGTH_SHORT).show();
|
|
||||||
LogUtils.i(TAG, "No albums found");
|
|
||||||
}
|
|
||||||
adapter.setData(albums);
|
|
||||||
LogUtils.d(TAG, "Loaded " + albums.size() + " albums");
|
|
||||||
}
|
|
||||||
|
|
||||||
private Uri getUriFromPath(String path) {
|
|
||||||
String[] projection = { MediaStore.Images.Media._ID };
|
|
||||||
String selection = MediaStore.Images.Media.DATA + " = ?";
|
|
||||||
String[] selectionArgs = { path };
|
|
||||||
try (Cursor cursor = getContentResolver().query(
|
|
||||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
|
||||||
projection, selection, selectionArgs, null)) {
|
|
||||||
if (cursor != null) {
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
|
|
||||||
return Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ArrayList<Uri> getImagesInFolder(String folderPath) {
|
|
||||||
ArrayList<Uri> imageUrls = new ArrayList<>();
|
|
||||||
ContentResolver contentResolver = getContentResolver();
|
|
||||||
Uri collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
|
||||||
|
|
||||||
String selection = MediaStore.Images.Media.DATA + " LIKE ?";
|
|
||||||
String[] selectionArgs = new String[]{folderPath + "/%"};
|
|
||||||
String sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC";
|
|
||||||
|
|
||||||
LogUtils.d(TAG, "getImagesInFolder: " + folderPath);
|
|
||||||
|
|
||||||
try (Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder)) {
|
|
||||||
if (cursor != null) {
|
|
||||||
LogUtils.d(TAG, "cursor count: " + cursor.getCount());
|
|
||||||
int dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
String path = cursor.getString(dataColumn);
|
|
||||||
if (path != null) {
|
|
||||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
|
|
||||||
Uri contentUri = Uri.withAppendedPath(collection, String.valueOf(id));
|
|
||||||
LogUtils.d(TAG, "image: id=" + id + ", path=" + path);
|
|
||||||
imageUrls.add(contentUri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LogUtils.d(TAG, "found " + imageUrls.size() + " images");
|
|
||||||
return imageUrls;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
getMenuInflater().inflate(R.menu.menu_main, menu);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
|
||||||
int id = item.getItemId();
|
|
||||||
if (id == R.id.action_mi_gallery) {
|
|
||||||
Toast.makeText(this, "Gallery clicked", Toast.LENGTH_SHORT).show();
|
|
||||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
|
||||||
intent.addCategory(Intent.CATEGORY_APP_GALLERY);
|
|
||||||
startActivity(intent);
|
|
||||||
return true;
|
|
||||||
} else if (id == R.id.action_change_bg_color) {
|
|
||||||
//Toast.makeText(this, "修改背景颜色", Toast.LENGTH_SHORT).show();
|
|
||||||
if (BackgroundUtils.DrawableType.COLOR == BackgroundUtils.getInstance().getDrawableType()) {
|
|
||||||
|
|
||||||
ColorPickerDialog dlg = new ColorPickerDialog(this, BackgroundUtils.getInstance().getColor());
|
|
||||||
dlg.setOnColorChangedListener(new com.a4455jkjh.colorpicker.view.OnColorChangedListener() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void beforeColorChanged() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onColorChanged(int color) {
|
|
||||||
BackgroundUtils.getInstance().initFromColor(MainActivity.this, color);
|
|
||||||
View content = findViewById(android.R.id.content);
|
|
||||||
if (content != null) {
|
|
||||||
content.setBackground(BackgroundUtils.getInstance().getDrawable());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterColorChanged() {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dlg.show();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} else if (id == R.id.action_settings) {
|
|
||||||
startActivity(new Intent(this, SettingsActivity.class));
|
|
||||||
return true;
|
|
||||||
} else if (id == R.id.action_about) {
|
|
||||||
startActivity(new Intent(this, AboutActivity.class));
|
|
||||||
return true;
|
|
||||||
} else if (id == R.id.action_trash) {
|
|
||||||
startActivity(new Intent(this, TrashActivity.class));
|
|
||||||
return true;
|
|
||||||
} else if (id == R.id.action_debug) {
|
|
||||||
LogActivity.startLogActivity(this);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private BroadcastReceiver coverUpdatedReceiver = new BroadcastReceiver() {
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (Preferences.ACTION_COVER_UPDATED.equals(intent.getAction())) {
|
|
||||||
loadAlbums();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
registerReceiver(coverUpdatedReceiver, new IntentFilter(Preferences.ACTION_COVER_UPDATED));
|
|
||||||
if (checkPermission()) {
|
|
||||||
scanMediaStore();
|
|
||||||
loadAlbums();
|
|
||||||
}
|
|
||||||
if (adapter != null) {
|
|
||||||
adapter.refreshBg();
|
|
||||||
adapter.refreshPinned();
|
|
||||||
adapter.refreshCover();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
unregisterReceiver(coverUpdatedReceiver);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void scanMediaStore() {
|
|
||||||
String folderPath = prefs.getFolderPath();
|
|
||||||
File baseFolder = new File(folderPath);
|
|
||||||
if (baseFolder.exists() && baseFolder.isDirectory()) {
|
|
||||||
File[] subfolders = baseFolder.listFiles(new FileFilter() {
|
|
||||||
@Override
|
|
||||||
public boolean accept(File file) {
|
|
||||||
return file.isDirectory();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (subfolders != null) {
|
|
||||||
ArrayList<String> paths = new ArrayList<>();
|
|
||||||
for (File subfolder : subfolders) {
|
|
||||||
File[] images = subfolder.listFiles(new FilenameFilter() {
|
|
||||||
@Override
|
|
||||||
public boolean accept(File dir, String name) {
|
|
||||||
String lower = name.toLowerCase();
|
|
||||||
return lower.endsWith(".jpg") || lower.endsWith(".jpeg")
|
|
||||||
|| lower.endsWith(".png") || lower.endsWith(".gif")
|
|
||||||
|| lower.endsWith(".webp") || lower.endsWith(".bmp");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (images != null) {
|
|
||||||
for (File img : images) {
|
|
||||||
paths.add(img.getAbsolutePath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!paths.isEmpty()) {
|
|
||||||
LogUtils.d(TAG, "scanning " + paths.size() + " files to MediaStore");
|
|
||||||
String[] pathArray = paths.toArray(new String[0]);
|
|
||||||
MediaScannerConnection.scanFile(this, pathArray, null, new MediaScannerConnection.OnScanCompletedListener() {
|
|
||||||
@Override
|
|
||||||
public void onScanCompleted(String path, Uri uri) {
|
|
||||||
LogUtils.d(TAG, "scanCompleted: " + path + " -> " + uri);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
package cc.winboll.studio.gallery;
|
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.provider.MediaStore;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import androidx.core.app.ActivityCompat;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
|
|
||||||
public class TrashActivity extends AppCompatActivity {
|
|
||||||
public static final String TAG = "TrashActivity";
|
|
||||||
private static final int PERMISSION_REQUEST_CODE = 102;
|
|
||||||
private RecyclerView recyclerView;
|
|
||||||
private TrashAdapter adapter;
|
|
||||||
private TrashManager trashManager;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_main);
|
|
||||||
LogUtils.d(TAG, "onCreate");
|
|
||||||
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
getSupportActionBar().setTitle("Trash");
|
|
||||||
|
|
||||||
trashManager = new TrashManager(this);
|
|
||||||
|
|
||||||
recyclerView = findViewById(R.id.recycler_view);
|
|
||||||
recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
|
|
||||||
adapter = new TrashAdapter();
|
|
||||||
recyclerView.setAdapter(adapter);
|
|
||||||
|
|
||||||
adapter.setOnTrashClickListener(new TrashAdapter.OnTrashClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onRestoreClick(int position) {
|
|
||||||
restoreImage(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDeleteClick(int position) {
|
|
||||||
permanentlyDelete(position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (checkPermission()) {
|
|
||||||
loadTrash();
|
|
||||||
} else {
|
|
||||||
requestPermission();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean checkPermission() {
|
|
||||||
return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
|
|
||||||
== PackageManager.PERMISSION_GRANTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void requestPermission() {
|
|
||||||
ActivityCompat.requestPermissions(this,
|
|
||||||
new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE},
|
|
||||||
PERMISSION_REQUEST_CODE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
|
||||||
@NonNull int[] grantResults) {
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
||||||
if (requestCode == PERMISSION_REQUEST_CODE) {
|
|
||||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
loadTrash();
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadTrash() {
|
|
||||||
LogUtils.d(TAG, "loadTrash");
|
|
||||||
Cursor cursor = trashManager.getTrashList();
|
|
||||||
ArrayList<TrashItem> items = new ArrayList<TrashItem>();
|
|
||||||
ArrayList<Uri> uris = new ArrayList<Uri>();
|
|
||||||
|
|
||||||
String trashPath = TrashDbHelper.getTrashPath();
|
|
||||||
File trashDir = new File(trashPath);
|
|
||||||
|
|
||||||
if (cursor != null && cursor.getCount() > 0) {
|
|
||||||
cursor.moveToFirst();
|
|
||||||
do {
|
|
||||||
try {
|
|
||||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
|
|
||||||
String fileName = cursor.getString(cursor.getColumnIndexOrThrow("file_name"));
|
|
||||||
String originalPath = cursor.getString(cursor.getColumnIndexOrThrow("original_path"));
|
|
||||||
String originalFolder = cursor.getString(cursor.getColumnIndexOrThrow("original_folder"));
|
|
||||||
|
|
||||||
TrashItem item = new TrashItem();
|
|
||||||
item.id = id;
|
|
||||||
item.fileName = fileName;
|
|
||||||
item.originalPath = originalPath;
|
|
||||||
item.originalFolder = originalFolder;
|
|
||||||
|
|
||||||
File trashFile = new File(trashDir, fileName);
|
|
||||||
if (trashFile.exists()) {
|
|
||||||
items.add(item);
|
|
||||||
uris.add(Uri.fromFile(trashFile));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
} while (cursor.moveToNext());
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter.setData(items, uris);
|
|
||||||
|
|
||||||
if (items.isEmpty()) {
|
|
||||||
Toast.makeText(this, "Trash is empty", Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void restoreImage(int position) {
|
|
||||||
LogUtils.d(TAG, "restoreImage: " + position);
|
|
||||||
long id = adapter.getItemId(position);
|
|
||||||
String fileName = adapter.getFileName(position);
|
|
||||||
String originalPath = adapter.getOriginalPath(position);
|
|
||||||
|
|
||||||
if (trashManager.restore(id, fileName, originalPath)) {
|
|
||||||
Toast.makeText(this, "Image restored", Toast.LENGTH_SHORT).show();
|
|
||||||
LogUtils.i(TAG, "Image restored");
|
|
||||||
loadTrash();
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this, "Restore failed", Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void permanentlyDelete(int position) {
|
|
||||||
long id = adapter.getItemId(position);
|
|
||||||
String fileName = adapter.getFileName(position);
|
|
||||||
|
|
||||||
if (trashManager.deletePermanently(id, fileName)) {
|
|
||||||
Toast.makeText(this, "Image deleted", Toast.LENGTH_SHORT).show();
|
|
||||||
loadTrash();
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this, "Delete failed", Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
getMenuInflater().inflate(R.menu.menu_trash, menu);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
|
||||||
if (item.getItemId() == R.id.action_clear_trash) {
|
|
||||||
trashManager.clearTrash();
|
|
||||||
Toast.makeText(this, "Trash cleared", Toast.LENGTH_SHORT).show();
|
|
||||||
loadTrash();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
if (checkPermission()) {
|
|
||||||
loadTrash();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class TrashItem {
|
|
||||||
public long id;
|
|
||||||
public String fileName;
|
|
||||||
public String originalPath;
|
|
||||||
public String originalFolder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
package cc.winboll.studio.gallery;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import com.bumptech.glide.Glide;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
|
|
||||||
public class TrashAdapter extends RecyclerView.Adapter<TrashAdapter.ViewHolder> {
|
|
||||||
public static final String TAG = "TrashAdapter";
|
|
||||||
private ArrayList<TrashActivity.TrashItem> trashItems = new ArrayList<TrashActivity.TrashItem>();
|
|
||||||
private ArrayList<Uri> imageUrls = new ArrayList<Uri>();
|
|
||||||
private OnTrashClickListener listener;
|
|
||||||
|
|
||||||
public interface OnTrashClickListener {
|
|
||||||
void onRestoreClick(int position);
|
|
||||||
void onDeleteClick(int position);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnTrashClickListener(OnTrashClickListener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setData(ArrayList<TrashActivity.TrashItem> items, ArrayList<Uri> uris) {
|
|
||||||
this.trashItems = items;
|
|
||||||
this.imageUrls = uris;
|
|
||||||
LogUtils.d(TAG, "setData: " + items.size() + " items");
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getItemId(int position) {
|
|
||||||
if (position >= 0 && position < trashItems.size()) {
|
|
||||||
return trashItems.get(position).id;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFileName(int position) {
|
|
||||||
if (position >= 0 && position < trashItems.size()) {
|
|
||||||
return trashItems.get(position).fileName;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getOriginalPath(int position) {
|
|
||||||
if (position >= 0 && position < trashItems.size()) {
|
|
||||||
return trashItems.get(position).originalPath;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_trash, parent, false);
|
|
||||||
return new ViewHolder(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
|
|
||||||
if (position < imageUrls.size()) {
|
|
||||||
Glide.with(holder.imageView.getContext())
|
|
||||||
.load(imageUrls.get(position))
|
|
||||||
.centerCrop()
|
|
||||||
.into(holder.imageView);
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.btnRestore.setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
if (listener != null) {
|
|
||||||
listener.onRestoreClick(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
holder.btnDelete.setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
if (listener != null) {
|
|
||||||
listener.onDeleteClick(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return trashItems.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
ImageView imageView;
|
|
||||||
ImageView btnRestore;
|
|
||||||
ImageView btnDelete;
|
|
||||||
ViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
imageView = itemView.findViewById(R.id.image);
|
|
||||||
btnRestore = itemView.findViewById(R.id.btn_restore);
|
|
||||||
btnDelete = itemView.findViewById(R.id.btn_delete);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="rectangle">
|
|
||||||
<solid android:color="#000000"/>
|
|
||||||
</shape>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="10dp"
|
|
||||||
android:height="10dp"
|
|
||||||
android:viewportWidth="10"
|
|
||||||
android:viewportHeight="10">
|
|
||||||
<path
|
|
||||||
android:fillColor="#808080"
|
|
||||||
android:pathData="M0,0h1v1h-1z M2,0h1v1h-1z M4,0h1v1h-1z M6,0h1v1h-1z M8,0h1v1h-1z
|
|
||||||
M1,1h1v1h-1z M3,1h1v1h-1z M5,1h1v1h-1z M7,1h1v1h-1z M9,1h1v1h-1z
|
|
||||||
M0,2h1v1h-1z M2,2h1v1h-1z M4,2h1v1h-1z M6,2h1v1h-1z M8,2h1v1h-1z
|
|
||||||
M1,3h1v1h-1z M3,3h1v1h-1z M5,3h1v1h-1z M7,3h1v1h-1z M9,3h1v1h-1z
|
|
||||||
M0,4h1v1h-1z M2,4h1v1h-1z M4,4h1v1h-1z M6,4h1v1h-1z M8,4h1v1h-1z
|
|
||||||
M1,5h1v1h-1z M3,5h1v1h-1z M5,5h1v1h-1z M7,5h1v1h-1z M9,5h1v1h-1z
|
|
||||||
M0,6h1v1h-1z M2,6h1v1h-1z M4,6h1v1h-1z M6,6h1v1h-1z M8,6h1v1h-1z
|
|
||||||
M1,7h1v1h-1z M3,7h1v1h-1z M5,7h1v1h-1z M7,7h1v1h-1z M9,7h1v1h-1z
|
|
||||||
M0,8h1v1h-1z M2,8h1v1h-1z M4,8h1v1h-1z M6,8h1v1h-1z M8,8h1v1h-1z
|
|
||||||
M1,9h1v1h-1z M3,9h1v1h-1z M5,9h1v1h-1z M7,9h1v1h-1z M9,9h1v1h-1z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M1,0h1v1h-1z M3,0h1v1h-1z M5,0h1v1h-1z M7,0h1v1h-1z M9,0h1v1h-1z
|
|
||||||
M0,1h1v1h-1z M2,1h1v1h-1z M4,1h1v1h-1z M6,1h1v1h-1z M8,1h1v1h-1z
|
|
||||||
M1,2h1v1h-1z M3,2h1v1h-1z M5,2h1v1h-1z M7,2h1v1h-1z M9,2h1v1h-1z
|
|
||||||
M0,3h1v1h-1z M2,3h1v1h-1z M4,3h1v1h-1z M6,3h1v1h-1z M8,3h1v1h-1z
|
|
||||||
M1,4h1v1h-1z M3,4h1v1h-1z M5,4h1v1h-1z M7,4h1v1h-1z M9,4h1v1h-1z
|
|
||||||
M0,5h1v1h-1z M2,5h1v1h-1z M4,5h1v1h-1z M6,5h1v1h-1z M8,5h1v1h-1z
|
|
||||||
M1,6h1v1h-1z M3,6h1v1h-1z M5,6h1v1h-1z M7,6h1v1h-1z M9,6h1v1h-1z
|
|
||||||
M0,7h1v1h-1z M2,7h1v1h-1z M4,7h1v1h-1z M6,7h1v1h-1z M8,7h1v1h-1z
|
|
||||||
M1,8h1v1h-1z M3,8h1v1h-1z M5,8h1v1h-1z M7,8h1v1h-1z M9,8h1v1h-1z
|
|
||||||
M0,9h1v1h-1z M2,9h1v1h-1z M4,9h1v1h-1z M6,9h1v1h-1z M8,9h1v1h-1z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="oval">
|
|
||||||
<solid android:color="#CCFFFFFF"/>
|
|
||||||
<stroke
|
|
||||||
android:width="1dp"
|
|
||||||
android:color="#000000"/>
|
|
||||||
</shape>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="rectangle">
|
|
||||||
<solid android:color="#FFFFFF"/>
|
|
||||||
</shape>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M4,4h7v7h-7z M13,4h7v7h-7z M4,13h7v7h-7z M13,13h7v7h-7z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#808080"
|
|
||||||
android:pathData="M11,4h2v7h-2z M4,11h7v2h-7z M13,11h7v2h-7z M11,13h2v7h-2z M13,4h2v7h-2z M4,13h7v2h-7z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M20.71,5.63l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-3.12,3.12 -1.93,-1.91 -1.41,1.41 1.42,1.42L3,16.25V21h4.75l8.92,-8.92 1.42,1.42 1.41,-1.41 -1.92,-1.92 3.12,-3.12c0.4,-0.4 0.4,-1.03 0.01,-1.42zM6.92,19H5v-1.92l8.06,-8.06 1.92,1.92L6.92,19z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M21,3H3C1.9,3 1,3.9 1,5v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V5C23,3.9 22.1,3 21,3zM21,19H3V5h18V19z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M9,12l2,2l4,-4l1.5,1.5L11,17l-3,-3z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,-1H5v2h14V4z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#000000"
|
|
||||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8s8,3.59 8,8s-3.59,8 -8,8z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#000000"
|
|
||||||
android:pathData="M12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5s5,-2.24 5,-5s-2.24,-5 -5,-5zM12,15c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3s3,1.34 3,3s-1.34,3 -3,3z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#000000"
|
|
||||||
android:pathData="M8,12l1.5,-2l2,2.5l2.5,-3l2,2.5"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M16,12V4H17V2H7V4H8V12L6,14V16H11.5V22H12.5V16H18V14L16,12Z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9H1l3.89,3.89 0.07,0.14L9,12H6c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,17.9 10.51,19 13,19c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08V8H12z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92s2.92,-1.31 2.92,-2.92 -1.31,-2.92 -2.92,-2.92z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M3,18h6v-2H3v2zM3,6v2h18V6H3zM3,13h12v-2H3v2z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportHeight="24"
|
|
||||||
android:viewportWidth="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M1,3V21H23V3H1M21,5V14H3V5H21M11,16V19H8V16H11M3,16H6V19H3V16M13,19V16H16V19H13M18,19V16H21V19H18Z"/>
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="@color/black">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="56dp"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:background="@color/colorPrimary"
|
|
||||||
android:paddingHorizontal="16dp">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/btn_close"
|
|
||||||
android:layout_width="40dp"
|
|
||||||
android:layout_height="40dp"
|
|
||||||
android:padding="5dp"
|
|
||||||
android:src="@drawable/ic_close"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/textview_window_name"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="封面剪裁"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textSize="18sp"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:gravity="center"
|
|
||||||
android:layout_marginEnd="8dp"/>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/btn_info"
|
|
||||||
android:layout_width="40dp"
|
|
||||||
android:layout_height="40dp"
|
|
||||||
android:padding="5dp"
|
|
||||||
android:src="@drawable/ic_info"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/btn_change_bg"
|
|
||||||
android:layout_width="40dp"
|
|
||||||
android:layout_height="40dp"
|
|
||||||
android:padding="5dp"
|
|
||||||
android:src="@drawable/ic_color_pick"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:contentDescription="修改剪裁背景颜色"/>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/btn_done"
|
|
||||||
android:layout_width="40dp"
|
|
||||||
android:layout_height="40dp"
|
|
||||||
android:padding="5dp"
|
|
||||||
android:src="@drawable/ic_done"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<ScrollView
|
|
||||||
android:id="@+id/scroll_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_marginTop="56dp"
|
|
||||||
android:layout_marginBottom="56dp"
|
|
||||||
android:fillViewport="true">
|
|
||||||
|
|
||||||
<cc.winboll.studio.gallery.ZoomContainerView
|
|
||||||
android:id="@+id/zoom_container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="center">
|
|
||||||
|
|
||||||
<cc.winboll.studio.gallery.CropCanvasView
|
|
||||||
android:id="@+id/crop_canvas_view"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"/>
|
|
||||||
|
|
||||||
</cc.winboll.studio.gallery.ZoomContainerView>
|
|
||||||
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="56dp"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:background="@color/colorPrimary"
|
|
||||||
android:paddingHorizontal="16dp"
|
|
||||||
android:layout_marginBottom="0dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="缩小"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textSize="12sp"/>
|
|
||||||
|
|
||||||
<SeekBar
|
|
||||||
android:id="@+id/seekbar_zoom"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:min="0"
|
|
||||||
android:max="100"
|
|
||||||
android:progress="50"
|
|
||||||
android:layout_marginHorizontal="8dp"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="放大"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textSize="12sp"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="@color/black"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true">
|
|
||||||
|
|
||||||
<androidx.viewpager.widget.ViewPager
|
|
||||||
android:id="@+id/view_pager"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clickable="false"/>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="top"
|
|
||||||
android:background="#CC000000"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:padding="4dp">
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/btn_back"
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:src="@drawable/ic_back"
|
|
||||||
android:contentDescription="Back"/>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/btn_gallery"
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:src="@drawable/ic_view_gallery_outline"
|
|
||||||
android:contentDescription="Gallery"/>
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/btn_share"
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:src="@drawable/ic_share"
|
|
||||||
android:contentDescription="Share"/>
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/btn_info"
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:src="@drawable/ic_info"
|
|
||||||
android:contentDescription="Info"/>
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/btn_delete"
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:src="@drawable/ic_delete"
|
|
||||||
android:contentDescription="Delete"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?attr/actionBarSize"
|
|
||||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/recycler_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="@android:color/transparent"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/fab_scroll_top"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|end"
|
|
||||||
android:layout_margin="16dp"
|
|
||||||
android:src="@drawable/ic_arrow_up"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:backgroundTint="@color/colorAccent"
|
|
||||||
app:tint="@color/white"/>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/folder_path"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:textStyle="bold"/>
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/edit_folder_path"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:hint="@string/enter_folder_path"
|
|
||||||
android:inputType="text"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_current_path"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:textColor="#888888"/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btn_save"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:text="@string/save"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@drawable/bg_dialog"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/title_text"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textStyle="bold"/>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp"
|
|
||||||
android:background="@drawable/bg_dialog"
|
|
||||||
android:gravity="center_horizontal">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/info_text"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:fontFamily="monospace"
|
|
||||||
android:textColor="@android:color/white"/>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/preview_image_container"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="2dp"
|
|
||||||
android:background="#FFBBD505">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/preview_image"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:scaleType="fitCenter"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="2dp">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/album_cover"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="120dp"
|
|
||||||
android:layout_margin="2dp"
|
|
||||||
android:scaleType="centerCrop"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/album_name"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:background="#80000000"
|
|
||||||
android:padding="4dp"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:ellipsize="end"/>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/pin_icon"
|
|
||||||
android:layout_width="24dp"
|
|
||||||
android:layout_height="24dp"
|
|
||||||
android:layout_gravity="bottom|end"
|
|
||||||
android:layout_margin="6dp"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:padding="3dp"
|
|
||||||
android:src="@drawable/ic_pin"
|
|
||||||
android:visibility="gone"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/image_count"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="top|end"
|
|
||||||
android:background="#80000000"
|
|
||||||
android:padding="4dp"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textSize="10sp"/>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="2dp">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/image"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="120dp"
|
|
||||||
android:scaleType="centerCrop"
|
|
||||||
android:background="@color/black"/>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/pin_icon"
|
|
||||||
android:layout_width="24dp"
|
|
||||||
android:layout_height="24dp"
|
|
||||||
android:layout_gravity="top|end"
|
|
||||||
android:layout_margin="4dp"
|
|
||||||
android:background="@drawable/bg_circle_white"
|
|
||||||
android:padding="4dp"
|
|
||||||
android:src="@drawable/ic_pin"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:tint="@color/black"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"/>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/cover_icon"
|
|
||||||
android:layout_width="24dp"
|
|
||||||
android:layout_height="24dp"
|
|
||||||
android:layout_gravity="bottom|end"
|
|
||||||
android:layout_margin="4dp"
|
|
||||||
android:background="@drawable/bg_circle_white"
|
|
||||||
android:padding="4dp"
|
|
||||||
android:src="@drawable/ic_cover"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:tint="@color/black"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"/>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/image"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:scaleType="fitCenter"/>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="2dp">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/image"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="120dp"
|
|
||||||
android:scaleType="centerCrop"
|
|
||||||
android:background="@color/black"/>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:background="#80000000"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/btn_restore"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="36dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:src="@drawable/ic_restore"
|
|
||||||
android:contentDescription="Restore"/>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/btn_delete"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="36dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:src="@drawable/ic_delete"
|
|
||||||
android:contentDescription="Delete"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_sort"
|
|
||||||
android:icon="@drawable/ic_sort"
|
|
||||||
android:title="排序"
|
|
||||||
app:showAsAction="ifRoom">
|
|
||||||
<menu>
|
|
||||||
<group android:checkableBehavior="single">
|
|
||||||
<item
|
|
||||||
android:id="@+id/sort_time_desc"
|
|
||||||
android:title="时间倒序"/>
|
|
||||||
<item
|
|
||||||
android:id="@+id/sort_time_asc"
|
|
||||||
android:title="时间正序"/>
|
|
||||||
<item
|
|
||||||
android:id="@+id/sort_name_desc"
|
|
||||||
android:title="名称倒序"/>
|
|
||||||
<item
|
|
||||||
android:id="@+id/sort_name_asc"
|
|
||||||
android:title="名称正序"/>
|
|
||||||
</group>
|
|
||||||
</menu>
|
|
||||||
</item>
|
|
||||||
</menu>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_done"
|
|
||||||
android:title="完成"
|
|
||||||
app:showAsAction="always"/>
|
|
||||||
</menu>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_mi_gallery"
|
|
||||||
android:title="@string/mi_gallery"
|
|
||||||
android:icon="@drawable/ic_mi_gallery"
|
|
||||||
app:showAsAction="ifRoom"/>
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_change_bg_color"
|
|
||||||
android:title="修改背景颜色"
|
|
||||||
app:showAsAction="never"/>
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_trash"
|
|
||||||
android:title="@string/trash"
|
|
||||||
app:showAsAction="never"/>
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_settings"
|
|
||||||
android:title="@string/settings"
|
|
||||||
app:showAsAction="never"/>
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_about"
|
|
||||||
android:title="@string/about"
|
|
||||||
app:showAsAction="never"/>
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_debug"
|
|
||||||
android:title="@string/debug_log"
|
|
||||||
app:showAsAction="never"/>
|
|
||||||
|
|
||||||
</menu>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_trash"
|
|
||||||
android:title="@string/trash"
|
|
||||||
app:showAsAction="never"/>
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_clear_trash"
|
|
||||||
android:title="@string/clear_trash"
|
|
||||||
app:showAsAction="never"/>
|
|
||||||
|
|
||||||
</menu>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="colorPrimary">#009688</color>
|
|
||||||
<color name="colorPrimaryDark">#00796B</color>
|
|
||||||
<color name="colorAccent">#FF9800</color>
|
|
||||||
<color name="black">#000000</color>
|
|
||||||
<color name="white">#FFFFFF</color>
|
|
||||||
</resources>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<resources>
|
|
||||||
<string name="app_name">Gallery</string>
|
|
||||||
<string name="app_description">WinBoLL Album App</string>
|
|
||||||
<string name="refresh">Refresh</string>
|
|
||||||
<string name="settings">Settings</string>
|
|
||||||
<string name="settings_title">Settings</string>
|
|
||||||
<string name="about">About</string>
|
|
||||||
<string name="folder_path">Folder Path</string>
|
|
||||||
<string name="enter_folder_path">Enter folder path</string>
|
|
||||||
<string name="save">Save</string>
|
|
||||||
<string name="cancel">Cancel</string>
|
|
||||||
<string name="no_images_found">No images found</string>
|
|
||||||
<string name="trash">Trash</string>
|
|
||||||
<string name="clear_trash">Clear Trash</string>
|
|
||||||
<string name="delete_confirm">Delete to trash?</string>
|
|
||||||
<string name="restore_confirm">Restore to original folder?</string>
|
|
||||||
<string name="yes">Yes</string>
|
|
||||||
<string name="no">No</string>
|
|
||||||
<string name="debug_log">Debug Log</string>
|
|
||||||
<string name="debug_message">Debug log message</string>
|
|
||||||
<string name="system_gallery">系统相册</string>
|
|
||||||
<string name="mi_gallery">小米相册</string>
|
|
||||||
<string name="reset_gallery">重置</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<resources>
|
|
||||||
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
|
||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="ColorPaletteDialog" parent="Theme.AppCompat.Light.NoActionBar">
|
|
||||||
<item name="android:windowBackground">@android:color/transparent</item>
|
|
||||||
<item name="android:windowIsFloating">false</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
1
positions/.gitignore
vendored
Normal file
1
positions/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
# Gallery
|
# Positions
|
||||||
|
|
||||||
#### 介绍
|
#### 介绍
|
||||||
云宝相册应用
|
安卓位置应用,有关于地理位置的相关应用。
|
||||||
|
PS:使用感言~~~『記低用唔到』。
|
||||||
|
|
||||||
#### 软件架构
|
#### 软件架构
|
||||||
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
||||||
@@ -10,7 +11,7 @@
|
|||||||
|
|
||||||
#### Gradle 编译说明
|
#### Gradle 编译说明
|
||||||
调试版编译命令 :gradle assembleBetaDebug
|
调试版编译命令 :gradle assembleBetaDebug
|
||||||
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh gallery
|
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh positions
|
||||||
|
|
||||||
#### 使用说明
|
#### 使用说明
|
||||||
|
|
||||||
@@ -18,20 +18,22 @@ def genVersionName(def versionName){
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
// 适配MIUI12
|
|
||||||
compileSdkVersion 30
|
// 关键:改为你已安装的 SDK 32(≥ targetSdkVersion 30,兼容已安装环境)
|
||||||
buildToolsVersion "30.0.3"
|
compileSdkVersion 32
|
||||||
|
|
||||||
|
// 直接使用已安装的构建工具 33.0.3(无需修改)
|
||||||
|
buildToolsVersion "33.0.3"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "cc.winboll.studio.gallery"
|
applicationId "cc.winboll.studio.positions"
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
// 适配MIUI12
|
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode 1
|
versionCode 1
|
||||||
// versionName 更新后需要手动设置
|
// versionName 更新后需要手动设置
|
||||||
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
||||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||||
versionName "15.0"
|
versionName "15.12"
|
||||||
if(true) {
|
if(true) {
|
||||||
versionName = genVersionName("${versionName}")
|
versionName = genVersionName("${versionName}")
|
||||||
}
|
}
|
||||||
@@ -41,20 +43,23 @@ android {
|
|||||||
packagingOptions {
|
packagingOptions {
|
||||||
doNotStrip "*/*/libmimo_1011.so"
|
doNotStrip "*/*/libmimo_1011.so"
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main {
|
|
||||||
jniLibs.srcDirs = ['libs'] // 若SO库放在libs目录下
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// 米盟
|
||||||
|
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
|
||||||
|
//注意:以下5个库必须要引入
|
||||||
|
//api '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'
|
||||||
|
|
||||||
api 'com.google.code.gson:gson:2.10.1'
|
// https://mvnrepository.com/artifact/com.jzxiang.pickerview/TimePickerDialog
|
||||||
|
api 'com.jzxiang.pickerview:TimePickerDialog:1.0.1'
|
||||||
|
|
||||||
// 下拉控件
|
// 谷歌定位服务核心依赖(FusedLocationProviderClient所在库)
|
||||||
api 'com.baoyz.pullrefreshlayout:library:1.2.0'
|
api 'com.google.android.gms:play-services-location:21.0.1'
|
||||||
|
|
||||||
// SSH
|
// SSH
|
||||||
api 'com.jcraft:jsch:0.1.55'
|
api 'com.jcraft:jsch:0.1.55'
|
||||||
@@ -65,55 +70,23 @@ dependencies {
|
|||||||
api 'com.journeyapps:zxing-android-embedded:3.6.0'
|
api 'com.journeyapps:zxing-android-embedded:3.6.0'
|
||||||
// 应用介绍页类库
|
// 应用介绍页类库
|
||||||
api 'io.github.medyo:android-about-page:2.0.0'
|
api 'io.github.medyo:android-about-page:2.0.0'
|
||||||
// 网络连接类库
|
// 网络连接类库
|
||||||
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
||||||
// OkHttp网络请求
|
// AndroidX 类库
|
||||||
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
|
api 'androidx.appcompat:appcompat:1.1.0'
|
||||||
// FastJSON解析
|
api 'com.google.android.material:material:1.4.0'
|
||||||
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.viewpager:viewpager:1.0.0'
|
||||||
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
|
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
|
||||||
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
|
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
|
||||||
//api 'androidx.fragment:fragment: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 地址
|
// WinBoLL库 nexus.winboll.cc 地址
|
||||||
api 'cc.winboll.studio:libaes:15.15.9'
|
api 'cc.winboll.studio:libaes:15.15.2'
|
||||||
api 'cc.winboll.studio:libappbase:15.15.21'
|
api 'cc.winboll.studio:libappbase:15.15.11'
|
||||||
|
|
||||||
// WinBoLL备用库 jitpack.io 地址
|
// WinBoLL备用库 jitpack.io 地址
|
||||||
//api 'com.github.ZhanGSKen:AES:aes-v15.15.7'
|
//api 'com.github.ZhanGSKen:AES:aes-v15.12.9'
|
||||||
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4'
|
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
|
||||||
|
|
||||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
}
|
}
|
||||||
8
positions/build.properties
Normal file
8
positions/build.properties
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
|
#Sat Apr 11 13:00:42 HKT 2026
|
||||||
|
stageCount=19
|
||||||
|
libraryProject=
|
||||||
|
baseVersion=15.12
|
||||||
|
publishVersion=15.12.18
|
||||||
|
buildCount=0
|
||||||
|
baseBetaVersion=15.12.19
|
||||||
@@ -67,6 +67,12 @@
|
|||||||
-keep class okio.** { *; }
|
-keep class okio.** { *; }
|
||||||
-dontwarn okhttp3.internal.platform.**
|
-dontwarn okhttp3.internal.platform.**
|
||||||
-dontwarn okio.**
|
-dontwarn okio.**
|
||||||
|
# ============================== 必要补充规则 ==============================
|
||||||
|
# OkHttp 4.4.1 补充规则(Java 7 兼容)
|
||||||
|
-keep class okhttp3.internal.concurrent.** { *; }
|
||||||
|
-keep class okhttp3.internal.connection.** { *; }
|
||||||
|
-dontwarn okhttp3.internal.concurrent.TaskRunner
|
||||||
|
-dontwarn okhttp3.internal.connection.RealCall
|
||||||
|
|
||||||
# Glide 4.9.0(米盟广告图片加载依赖)
|
# Glide 4.9.0(米盟广告图片加载依赖)
|
||||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools" >
|
xmlns:tools="http://schemas.android.com/tools" >
|
||||||
|
|
||||||
<application>
|
<application
|
||||||
|
tools:replace="android:icon"
|
||||||
|
android:icon="@drawable/ic_launcher_beta">
|
||||||
|
|
||||||
<!-- Put flavor specific code here -->
|
<!-- Put flavor specific code here -->
|
||||||
|
|
||||||
5
positions/src/beta/res/values-zh/strings.xml
Normal file
5
positions/src/beta/res/values-zh/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">悟空笔记#</string>
|
||||||
|
<string name="appplus_name">时空任务#</string>
|
||||||
|
</resources>
|
||||||
7
positions/src/beta/res/values/strings.xml
Normal file
7
positions/src/beta/res/values/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<string name="app_name">Positions</string>
|
||||||
|
<string name="appplus_name">PositionsPlus+</string>
|
||||||
|
|
||||||
|
</resources>
|
||||||
6
positions/src/beta/res/xml/file_provider.xml
Normal file
6
positions/src/beta/res/xml/file_provider.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<external-files-path
|
||||||
|
name="BaseBean"
|
||||||
|
path="BaseBean/" />
|
||||||
|
</paths>
|
||||||
19
positions/src/beta/res/xml/shortcutsmain.xml
Normal file
19
positions/src/beta/res/xml/shortcutsmain.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- 切换启动入口的快捷菜单 -->
|
||||||
|
<shortcut
|
||||||
|
android:shortcutId="open_appplus"
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:shortcutShortLabel="@string/open_appplus"
|
||||||
|
android:shortcutLongLabel="@string/open_appplus"
|
||||||
|
android:shortcutDisabledMessage="@string/appplus_open_disabled">
|
||||||
|
<intent
|
||||||
|
android:action="cc.winboll.studio.positions.App.ACTION_OPEN_APPPLUS"
|
||||||
|
android:targetPackage="cc.winboll.studio.positions.beta"
|
||||||
|
android:targetClass="cc.winboll.studio.positions.activities.ShortcutActionActivity"
|
||||||
|
android:data="open_appplus" />
|
||||||
|
|
||||||
|
<categories android:name="android.shortcut.conversation" />
|
||||||
|
</shortcut>
|
||||||
|
</shortcuts>
|
||||||
19
positions/src/beta/res/xml/shortcutsplus.xml
Normal file
19
positions/src/beta/res/xml/shortcutsplus.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- 切换启动入口的快捷菜单 -->
|
||||||
|
<shortcut
|
||||||
|
android:shortcutId="close_appplus"
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:shortcutShortLabel="@string/close_appplus"
|
||||||
|
android:shortcutLongLabel="@string/close_appplus"
|
||||||
|
android:shortcutDisabledMessage="@string/appplus_close_disabled">
|
||||||
|
<intent
|
||||||
|
android:action="cc.winboll.studio.positions.App.ACTION_CLOSE_APPPLUS"
|
||||||
|
android:targetPackage="cc.winboll.studio.positions.beta"
|
||||||
|
android:targetClass="cc.winboll.studio.positions.activities.ShortcutActionActivity"
|
||||||
|
android:data="close_appplus" />
|
||||||
|
|
||||||
|
<categories android:name="android.shortcut.conversation" />
|
||||||
|
</shortcut>
|
||||||
|
</shortcuts>
|
||||||
164
positions/src/main/AndroidManifest.xml
Normal file
164
positions/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="cc.winboll.studio.positions">
|
||||||
|
|
||||||
|
<!-- 只有在前台运行时才能获取大致位置信息 -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||||
|
|
||||||
|
<!-- 在后台使用位置信息 -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
||||||
|
|
||||||
|
<!-- 运行前台服务 -->
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
|
|
||||||
|
<!-- 运行“location”类型的前台服务 -->
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
|
||||||
|
|
||||||
|
<!-- 拥有完全的网络访问权限 -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
|
||||||
|
<!-- 安装快捷方式 -->
|
||||||
|
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
|
||||||
|
|
||||||
|
<!-- 读取您共享存储空间中的内容 -->
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
|
||||||
|
<!-- 修改或删除您共享存储空间中的内容 -->
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
|
||||||
|
<!-- 只能在前台获取精确的位置信息 -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
|
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.location.gps"
|
||||||
|
android:required="false"/>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@drawable/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/MyAppTheme"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
android:name=".App">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:exported="true">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
|
||||||
|
<action android:name="android.intent.action.SEND"/>
|
||||||
|
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
|
||||||
|
<action android:name="android.intent.action.EDIT"/>
|
||||||
|
|
||||||
|
<data android:mimeType="application/json"/>
|
||||||
|
|
||||||
|
<data android:mimeType="text/x-json"/>
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity-alias
|
||||||
|
android:name=".MainActivityWukong"
|
||||||
|
android:targetActivity=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:icon="@drawable/ic_launcher"
|
||||||
|
android:enabled="true">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcutsmain"/>
|
||||||
|
|
||||||
|
</activity-alias>
|
||||||
|
|
||||||
|
<activity-alias
|
||||||
|
android:name=".MainActivityLaojun"
|
||||||
|
android:targetActivity=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/appplus_name"
|
||||||
|
android:icon="@drawable/ic_positions_plus"
|
||||||
|
android:enabled="false">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcutsplus"/>
|
||||||
|
|
||||||
|
</activity-alias>
|
||||||
|
|
||||||
|
<activity android:name="cc.winboll.studio.positions.activities.LocationActivity"/>
|
||||||
|
|
||||||
|
<activity android:name="cc.winboll.studio.positions.activities.ShortcutActionActivity"/>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".services.MainService"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".services.AssistantService"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".services.DistanceRefreshService"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<receiver android:name="cc.winboll.studio.positions.receivers.MotionStatusReceiver">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
|
||||||
|
<action android:name="cc.winboll.studio.positions.receivers.MotionStatusReceiver"/>
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.max_aspect"
|
||||||
|
android:value="4.0"/>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.gms.version"
|
||||||
|
android:value="@integer/google_play_services_version"/>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_provider"/>
|
||||||
|
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
<activity android:name="cc.winboll.studio.positions.activities.SettingsActivity"/>
|
||||||
|
|
||||||
|
<activity android:name="cc.winboll.studio.positions.activities.AboutActivity"/>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
348
positions/src/main/java/cc/winboll/studio/positions/App.java
Normal file
348
positions/src/main/java/cc/winboll/studio/positions/App.java
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
package cc.winboll.studio.positions;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.HorizontalScrollView;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||||
|
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||||
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.lang.Thread.UncaughtExceptionHandler;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public class App extends GlobalApplication {
|
||||||
|
|
||||||
|
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
setIsDebugging(BuildConfig.DEBUG);
|
||||||
|
|
||||||
|
WinBoLLActivityManager.init(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void write(InputStream input, OutputStream output) throws IOException {
|
||||||
|
byte[] buf = new byte[1024 * 8];
|
||||||
|
int len;
|
||||||
|
while ((len = input.read(buf)) != -1) {
|
||||||
|
output.write(buf, 0, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void write(File file, byte[] data) throws IOException {
|
||||||
|
File parent = file.getParentFile();
|
||||||
|
if (parent != null && !parent.exists()) parent.mkdirs();
|
||||||
|
|
||||||
|
ByteArrayInputStream input = new ByteArrayInputStream(data);
|
||||||
|
FileOutputStream output = new FileOutputStream(file);
|
||||||
|
try {
|
||||||
|
write(input, output);
|
||||||
|
} finally {
|
||||||
|
closeIO(input, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toString(InputStream input) throws IOException {
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
write(input, output);
|
||||||
|
try {
|
||||||
|
return output.toString("UTF-8");
|
||||||
|
} finally {
|
||||||
|
closeIO(input, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void closeIO(Closeable... closeables) {
|
||||||
|
for (Closeable closeable : closeables) {
|
||||||
|
try {
|
||||||
|
if (closeable != null) closeable.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CrashHandler {
|
||||||
|
|
||||||
|
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
|
||||||
|
|
||||||
|
private static CrashHandler sInstance;
|
||||||
|
|
||||||
|
private PartCrashHandler mPartCrashHandler;
|
||||||
|
|
||||||
|
public static CrashHandler getInstance() {
|
||||||
|
if (sInstance == null) {
|
||||||
|
sInstance = new CrashHandler();
|
||||||
|
}
|
||||||
|
return sInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerGlobal(Context context) {
|
||||||
|
registerGlobal(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerGlobal(Context context, String crashDir) {
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregister() {
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerPart(Context context) {
|
||||||
|
unregisterPart(context);
|
||||||
|
mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
|
||||||
|
MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregisterPart(Context context) {
|
||||||
|
if (mPartCrashHandler != null) {
|
||||||
|
mPartCrashHandler.isRunning.set(false);
|
||||||
|
mPartCrashHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PartCrashHandler implements Runnable {
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
|
||||||
|
public AtomicBoolean isRunning = new AtomicBoolean(true);
|
||||||
|
|
||||||
|
public PartCrashHandler(Context context) {
|
||||||
|
this.mContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (isRunning.get()) {
|
||||||
|
try {
|
||||||
|
Looper.loop();
|
||||||
|
} catch (final Throwable e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
if (isRunning.get()) {
|
||||||
|
MAIN_HANDLER.post(new Runnable(){
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (e instanceof RuntimeException) {
|
||||||
|
throw (RuntimeException)e;
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
|
||||||
|
|
||||||
|
private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
|
||||||
|
private final File mCrashDir;
|
||||||
|
|
||||||
|
public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
|
||||||
|
this.mContext = context;
|
||||||
|
this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void uncaughtException(Thread thread, Throwable throwable) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
String log = buildLog(throwable);
|
||||||
|
writeLog(log);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Intent intent = new Intent(mContext, CrashActivity.class);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intent.putExtra(Intent.EXTRA_TEXT, log);
|
||||||
|
mContext.startActivity(intent);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
writeLog(e.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
throwable.printStackTrace();
|
||||||
|
android.os.Process.killProcess(android.os.Process.myPid());
|
||||||
|
System.exit(0);
|
||||||
|
|
||||||
|
} catch (Throwable e) {
|
||||||
|
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildLog(Throwable throwable) {
|
||||||
|
String time = DATE_FORMAT.format(new Date());
|
||||||
|
|
||||||
|
String versionName = "unknown";
|
||||||
|
long versionCode = 0;
|
||||||
|
try {
|
||||||
|
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
|
||||||
|
versionName = packageInfo.versionName;
|
||||||
|
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
|
||||||
|
} catch (Throwable ignored) {}
|
||||||
|
|
||||||
|
LinkedHashMap<String, String> head = new LinkedHashMap<String, String>();
|
||||||
|
head.put("Time Of Crash", time);
|
||||||
|
head.put("Device", String.format("%s, %s", Build.MANUFACTURER, Build.MODEL));
|
||||||
|
head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
|
||||||
|
head.put("App Version", String.format("%s (%d)", versionName, versionCode));
|
||||||
|
head.put("Kernel", getKernel());
|
||||||
|
head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS): "unknown");
|
||||||
|
head.put("Fingerprint", Build.FINGERPRINT);
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
|
||||||
|
for (String key : head.keySet()) {
|
||||||
|
if (builder.length() != 0) builder.append("\n");
|
||||||
|
builder.append(key);
|
||||||
|
builder.append(" : ");
|
||||||
|
builder.append(head.get(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.append("\n\n");
|
||||||
|
builder.append(Log.getStackTraceString(throwable));
|
||||||
|
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeLog(String log) {
|
||||||
|
String time = DATE_FORMAT.format(new Date());
|
||||||
|
File file = new File(mCrashDir, "crash_" + time + ".txt");
|
||||||
|
try {
|
||||||
|
write(file, log.getBytes("UTF-8"));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getKernel() {
|
||||||
|
try {
|
||||||
|
return App.toString(new FileInputStream("/proc/version")).trim();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class CrashActivity extends Activity {
|
||||||
|
|
||||||
|
private String mLog;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
setTheme(android.R.style.Theme_DeviceDefault);
|
||||||
|
setTitle("App Crash");
|
||||||
|
|
||||||
|
mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||||
|
|
||||||
|
ScrollView contentView = new ScrollView(this);
|
||||||
|
contentView.setFillViewport(true);
|
||||||
|
|
||||||
|
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
|
||||||
|
|
||||||
|
TextView textView = new TextView(this);
|
||||||
|
int padding = dp2px(16);
|
||||||
|
textView.setPadding(padding, padding, padding, padding);
|
||||||
|
textView.setText(mLog);
|
||||||
|
textView.setTextIsSelectable(true);
|
||||||
|
textView.setTypeface(Typeface.DEFAULT);
|
||||||
|
textView.setLinksClickable(true);
|
||||||
|
|
||||||
|
horizontalScrollView.addView(textView);
|
||||||
|
contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||||
|
|
||||||
|
setContentView(contentView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restart() {
|
||||||
|
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
|
||||||
|
if (intent != null) {
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
android.os.Process.killProcess(android.os.Process.myPid());
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int dp2px(float dpValue) {
|
||||||
|
final float scale = Resources.getSystem().getDisplayMetrics().density;
|
||||||
|
return (int) (dpValue * scale + 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
menu.add(0, android.R.id.copy, 0, android.R.string.copy)
|
||||||
|
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||||
|
return super.onCreateOptionsMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case android.R.id.copy:
|
||||||
|
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
package cc.winboll.studio.positions;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.CompoundButton;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.Switch;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||||
|
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||||
|
import cc.winboll.studio.libaes.utils.DevelopUtils;
|
||||||
|
import cc.winboll.studio.libaes.views.ADsBannerView;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.positions.R;
|
||||||
|
import cc.winboll.studio.positions.activities.AboutActivity;
|
||||||
|
import cc.winboll.studio.positions.activities.LocationActivity;
|
||||||
|
import cc.winboll.studio.positions.activities.SettingsActivity;
|
||||||
|
import cc.winboll.studio.positions.activities.WinBoLLActivity;
|
||||||
|
import cc.winboll.studio.positions.utils.AppConfigsUtil;
|
||||||
|
import cc.winboll.studio.positions.utils.ServiceUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主页面:仅负责
|
||||||
|
* 1. 位置服务启动/停止(通过 Switch 开关控制)
|
||||||
|
* 2. 跳转至“位置管理页(LocationActivity)”和“日志页(LogActivity)”
|
||||||
|
* 3. Java 7 语法适配:无 Lambda、显式接口实现、兼容低版本
|
||||||
|
*/
|
||||||
|
public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||||
|
public static final String TAG = "MainActivity";
|
||||||
|
// 权限请求码(建议定义为类常量,避免魔法值)
|
||||||
|
private static final int REQUEST_LOCATION_PERMISSIONS = 1001;
|
||||||
|
private static final int REQUEST_BACKGROUND_LOCATION_PERMISSION = 1002;
|
||||||
|
|
||||||
|
// UI 控件:服务控制开关、顶部工具栏
|
||||||
|
private Switch mServiceSwitch;
|
||||||
|
private Button mManagePositionsButton;
|
||||||
|
private Toolbar mToolbar;
|
||||||
|
// 服务相关:服务实例、绑定状态标记
|
||||||
|
//private DistanceRefreshService mDistanceService;
|
||||||
|
private boolean isServiceBound = false;
|
||||||
|
ADsBannerView mADsBannerView;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Activity getActivity() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTag() {
|
||||||
|
return TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- 服务连接回调(仅用于获取服务状态,不依赖服务执行核心逻辑) ----------------------
|
||||||
|
// private final ServiceConnection mServiceConn = new ServiceConnection() {
|
||||||
|
// /**
|
||||||
|
// * 服务绑定成功:获取服务实例,同步开关状态(以服务实际状态为准)
|
||||||
|
// */
|
||||||
|
// @Override
|
||||||
|
// public void onServiceConnected(ComponentName name, IBinder service) {
|
||||||
|
// // Java 7 显式强转 Binder 实例(确保类型匹配,避免ClassCastException)
|
||||||
|
// DistanceRefreshService.DistanceBinder binder = (DistanceRefreshService.DistanceBinder) service;
|
||||||
|
// mDistanceService = binder.getService();
|
||||||
|
// isServiceBound = true;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * 服务意外断开(如服务崩溃):重置服务实例和绑定状态
|
||||||
|
// */
|
||||||
|
// @Override
|
||||||
|
// public void onServiceDisconnected(ComponentName name) {
|
||||||
|
// mDistanceService = null;
|
||||||
|
// isServiceBound = false;
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// ---------------------- Activity 生命周期(核心:初始化UI、申请权限、绑定服务、释放资源) ----------------------
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_main); // 关联主页面布局
|
||||||
|
|
||||||
|
// 1. 初始化顶部 Toolbar(保留原逻辑,设置页面标题)
|
||||||
|
initToolbar();
|
||||||
|
// 2. 初始化其他控件
|
||||||
|
initViews();
|
||||||
|
// 3. 检查并申请位置权限(含后台GPS权限,确保服务启动前权限就绪)
|
||||||
|
if (!checkLocationPermissions()) {
|
||||||
|
requestLocationPermissions();
|
||||||
|
}
|
||||||
|
// 4. 绑定服务(仅用于获取服务实时状态,不影响服务独立运行)
|
||||||
|
//bindDistanceService();
|
||||||
|
|
||||||
|
mADsBannerView = findViewById(R.id.adsbanner);
|
||||||
|
|
||||||
|
setLLMainBackgroundColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在 Activity 的 onCreate() 或需要获取颜色的方法中调用
|
||||||
|
private void setLLMainBackgroundColor() {
|
||||||
|
// 1. 定义要解析的主题属性(这里是 colorAccent)
|
||||||
|
TypedArray a = getTheme().obtainStyledAttributes(new int[]{android.R.attr.colorAccent});
|
||||||
|
// 2. 获取对应的颜色值(默认值可设为你需要的 fallback 颜色,如 Color.GRAY)
|
||||||
|
int colorAccent = a.getColor(0, Color.GRAY);
|
||||||
|
// 3. 必须回收,避免内存泄漏
|
||||||
|
a.recycle();
|
||||||
|
|
||||||
|
LinearLayout llmain = findViewById(R.id.llmain);
|
||||||
|
llmain.setBackgroundColor(colorAccent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (mADsBannerView != null) {
|
||||||
|
mADsBannerView.releaseAdResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面销毁时解绑服务,避免Activity与服务相互引用导致内存泄漏
|
||||||
|
// if (isServiceBound) {
|
||||||
|
// unbindService(mServiceConn);
|
||||||
|
// isServiceBound = false;
|
||||||
|
// mDistanceService = null;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (mADsBannerView != null) {
|
||||||
|
mADsBannerView.resumeADs(MainActivity.this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------- 核心功能1:初始化UI组件(Toolbar + 服务开关) ----------------------
|
||||||
|
/**
|
||||||
|
* 初始化顶部 Toolbar,设置页面标题
|
||||||
|
*/
|
||||||
|
private void initToolbar() {
|
||||||
|
mToolbar = (Toolbar) findViewById(R.id.toolbar); // Java 7 显式 findViewById + 强转
|
||||||
|
setSupportActionBar(mToolbar);
|
||||||
|
// 给ActionBar设置标题(先判断非空,避免空指针异常)
|
||||||
|
if (getSupportActionBar() != null) {
|
||||||
|
getSupportActionBar().setTitle(getString(R.string.app_name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化服务控制开关:读取SP状态、绑定点击事件(含权限检查)
|
||||||
|
*/
|
||||||
|
private void initViews() {
|
||||||
|
mServiceSwitch = (Switch) findViewById(R.id.switch_service_control); // 显式强转
|
||||||
|
mServiceSwitch.setChecked(AppConfigsUtil.getInstance(this).isEnableMainService(true));
|
||||||
|
|
||||||
|
mManagePositionsButton = (Button) findViewById(R.id.btn_manage_positions);
|
||||||
|
mManagePositionsButton.setEnabled(mServiceSwitch.isChecked());
|
||||||
|
|
||||||
|
// Java 7 用匿名内部类实现 CompoundButton.OnCheckedChangeListener
|
||||||
|
mServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||||
|
// 开关打开前先检查权限:无权限则终止操作、重置开关、引导申请
|
||||||
|
if (isChecked && !checkLocationPermissions()) {
|
||||||
|
requestLocationPermissions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 权限就绪:执行服务启停逻辑
|
||||||
|
if (isChecked) {
|
||||||
|
LogUtils.d(TAG, "设置启动服务");
|
||||||
|
ServiceUtil.startAutoService(MainActivity.this);
|
||||||
|
} else {
|
||||||
|
LogUtils.d(TAG, "设置关闭服务");
|
||||||
|
|
||||||
|
ServiceUtil.stopAutoService(MainActivity.this);
|
||||||
|
}
|
||||||
|
|
||||||
|
mManagePositionsButton.setEnabled(isChecked);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
// 主题菜单
|
||||||
|
AESThemeUtil.inflateMenu(this, menu);
|
||||||
|
// 调试工具菜单
|
||||||
|
if (App.isDebugging()) {
|
||||||
|
DevelopUtils.inflateMenu(this, menu);
|
||||||
|
}
|
||||||
|
// 应用其他菜单
|
||||||
|
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
int menuItemId = item.getItemId();
|
||||||
|
if (AESThemeUtil.onAppThemeItemSelected(this, item)) {
|
||||||
|
recreate();
|
||||||
|
} if (DevelopUtils.onDevelopItemSelected(this, item)) {
|
||||||
|
LogUtils.d(TAG, String.format("onOptionsItemSelected item.getItemId() %d ", item.getItemId()));
|
||||||
|
} else if (menuItemId == R.id.item_settings) {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setClass(this, SettingsActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
} else if (menuItemId == R.id.item_about) {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setClass(this, AboutActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
} else {
|
||||||
|
// 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定服务(仅用于获取服务状态,不启动服务)
|
||||||
|
*/
|
||||||
|
// private void bindDistanceService() {
|
||||||
|
// Intent serviceIntent = new Intent(this, MainService.class);
|
||||||
|
// // BIND_AUTO_CREATE:服务未启动则创建(仅为获取状态,启停由开关控制)
|
||||||
|
// bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ---------------------- 核心功能3:页面跳转(位置管理页+日志页) ----------------------
|
||||||
|
/**
|
||||||
|
* 跳转至“位置管理页(LocationActivity)”(按钮点击触发,需在布局中设置 android:onClick="onPositions")
|
||||||
|
* 服务未启动时提示,不允许跳转(避免LocationActivity无数据)
|
||||||
|
*/
|
||||||
|
public void onPositions(View view) {
|
||||||
|
//ToastUtils.show("onPositions");
|
||||||
|
// 服务已启动:跳转到位置管理页
|
||||||
|
startActivity(new Intent(MainActivity.this, LocationActivity.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- 新增:位置权限处理(适配Java7 + 后台GPS权限) ----------------------
|
||||||
|
/**
|
||||||
|
* 检查是否拥有「前台+后台」位置权限(适配Android版本差异)
|
||||||
|
* Java7 特性:显式类型判断、无Lambda、兼容低版本API
|
||||||
|
*/
|
||||||
|
private boolean checkLocationPermissions() {
|
||||||
|
// 1. 检查前台精确定位权限(Android 6.0+ 必需,显式强转权限常量)
|
||||||
|
int foregroundPermResult = checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION);
|
||||||
|
boolean hasForegroundPerm = (foregroundPermResult == PackageManager.PERMISSION_GRANTED);
|
||||||
|
|
||||||
|
// 2. 检查后台定位权限(仅Android 10+ 需要,Java7 显式用Build.VERSION判断版本)
|
||||||
|
boolean hasBackgroundPerm = true;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
int backgroundPermResult = checkSelfPermission(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION);
|
||||||
|
hasBackgroundPerm = (backgroundPermResult == PackageManager.PERMISSION_GRANTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前台+后台权限均满足,才返回true
|
||||||
|
return hasForegroundPerm && hasBackgroundPerm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestLocationPermissions() {
|
||||||
|
// 1. 先判断前台定位权限(ACCESS_FINE_LOCATION)是否已授予
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
// 1.1 未授予前台权限:先申请前台权限(API 30+ 后台权限依赖前台权限)
|
||||||
|
String[] foregroundPermissions = new String[]{Manifest.permission.ACCESS_FINE_LOCATION};
|
||||||
|
// 对API 23+(Android 6.0)动态申请,低版本会直接授予(清单已声明前提下)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
requestPermissions(foregroundPermissions, REQUEST_LOCATION_PERMISSIONS);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 2. 已授予前台权限:判断是否需要申请后台权限(仅API 29+需要)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
// 2.1 检查后台权限是否未授予
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
// 2.2 API 30+ 必须单独申请后台权限(不能和前台权限一起弹框)
|
||||||
|
requestPermissions(
|
||||||
|
new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION},
|
||||||
|
REQUEST_BACKGROUND_LOCATION_PERMISSION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3. 前台权限已授予(+ 后台权限按需授予):此处可执行定位相关逻辑
|
||||||
|
// doLocationRelatedLogic();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 【必须补充】权限申请结果回调(处理用户同意/拒绝逻辑)
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
// 处理前台权限申请结果
|
||||||
|
if (requestCode == REQUEST_LOCATION_PERMISSIONS) {
|
||||||
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
// 前台权限同意:自动尝试申请后台权限(如果是API 29+)
|
||||||
|
requestLocationPermissions();
|
||||||
|
} else {
|
||||||
|
// 前台权限拒绝:提示用户(可选:引导跳转到应用设置页)
|
||||||
|
Toast.makeText(this, "需要前台定位权限才能使用该功能", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
} else if (requestCode == REQUEST_BACKGROUND_LOCATION_PERMISSION) {
|
||||||
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
// 后台权限同意:可执行后台定位逻辑
|
||||||
|
Toast.makeText(this, "已获得后台定位权限", Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
// 后台权限拒绝:提示用户(可选:说明后台定位的用途,引导手动开启)
|
||||||
|
Toast.makeText(this, "拒绝后台权限将无法在后台持续定位", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package cc.winboll.studio.positions.activities;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
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;
|
||||||
|
import cc.winboll.studio.positions.R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/01/13 11:25
|
||||||
|
* @Describe 应用介绍窗口
|
||||||
|
*/
|
||||||
|
public class AboutActivity extends WinBoLLActivity {
|
||||||
|
|
||||||
|
public static final String TAG = "AboutActivity";
|
||||||
|
private Toolbar mToolbar;
|
||||||
|
@Override
|
||||||
|
public Activity getActivity() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTag() {
|
||||||
|
return TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_about);
|
||||||
|
|
||||||
|
// 设置工具栏
|
||||||
|
initToolbar();
|
||||||
|
|
||||||
|
AboutView aboutView = findViewById(R.id.aboutview);
|
||||||
|
aboutView.setAPPInfo(genDefaultAppInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initToolbar() {
|
||||||
|
LogUtils.d(TAG, "initToolbar() 开始初始化");
|
||||||
|
mToolbar = findViewById(R.id.toolbar);
|
||||||
|
if (mToolbar == null) {
|
||||||
|
LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSupportActionBar(mToolbar);
|
||||||
|
mToolbar.setSubtitle(getTag());
|
||||||
|
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||||
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
LogUtils.d(TAG, "导航栏 点击返回按钮");
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
LogUtils.d(TAG, "initToolbar() 配置完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
private APPInfo genDefaultAppInfo() {
|
||||||
|
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
|
||||||
|
String branchName = "positions";
|
||||||
|
APPInfo appInfo = new APPInfo();
|
||||||
|
appInfo.setAppName(getString(R.string.app_name));
|
||||||
|
appInfo.setAppIcon(R.drawable.ic_winboll);
|
||||||
|
appInfo.setAppDescription(getString(R.string.app_description));
|
||||||
|
appInfo.setAppGitName("Positions");
|
||||||
|
appInfo.setAppGitOwner("Studio");
|
||||||
|
appInfo.setAppGitAPPBranch(branchName);
|
||||||
|
appInfo.setAppGitAPPSubProjectFolder(branchName);
|
||||||
|
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=Positions");
|
||||||
|
appInfo.setAppAPKName("Positions");
|
||||||
|
appInfo.setAppAPKFolderName("Positions");
|
||||||
|
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
|
||||||
|
return appInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,509 @@
|
|||||||
|
package cc.winboll.studio.positions.activities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
|
* @Date 2025/09/29 18:22
|
||||||
|
* @Describe 位置列表页面(适配MainService GPS接口+规范服务交互+完善生命周期)
|
||||||
|
*/
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
|
||||||
|
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
|
import cc.winboll.studio.positions.MainActivity;
|
||||||
|
import cc.winboll.studio.positions.R;
|
||||||
|
import cc.winboll.studio.positions.adapters.PositionAdapter;
|
||||||
|
import cc.winboll.studio.positions.models.PositionModel;
|
||||||
|
import cc.winboll.studio.positions.services.MainService;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Java 7 语法适配:
|
||||||
|
* 1. 服务绑定用匿名内部类实现 ServiceConnection
|
||||||
|
* 2. Adapter 初始化传入 MainService 实例,确保数据来源唯一
|
||||||
|
* 3. 所有位置/任务操作通过 MainService 接口执行
|
||||||
|
*/
|
||||||
|
public class LocationActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||||
|
public static final String TAG = "LocationActivity";
|
||||||
|
|
||||||
|
private Toolbar mToolbar;
|
||||||
|
|
||||||
|
private RecyclerView mRvPosition;
|
||||||
|
private PositionAdapter mPositionAdapter;
|
||||||
|
|
||||||
|
// MainService 引用+绑定状态(AtomicBoolean 确保多线程状态可见性)
|
||||||
|
private MainService mMainService;
|
||||||
|
private final AtomicBoolean isServiceBound = new AtomicBoolean(false);
|
||||||
|
// 标记 Adapter 是否已初始化(避免重复初始化/销毁后初始化)
|
||||||
|
private final AtomicBoolean isAdapterInited = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
// ---------------------- 新增:GPS监听核心变量 ----------------------
|
||||||
|
private MainService.GpsUpdateListener mGpsUpdateListener; // GPS监听实例
|
||||||
|
private PositionModel mCurrentGpsPos; // 缓存当前GPS位置(供页面使用)
|
||||||
|
// 本地位置缓存(解决服务数据未同步时Adapter空数据问题)
|
||||||
|
private final ArrayList<PositionModel> mLocalPosCache = new ArrayList<PositionModel>();
|
||||||
|
|
||||||
|
|
||||||
|
// 服务连接(Java 7 匿名内部类实现,强化状态同步+数据预加载)
|
||||||
|
private ServiceConnection mServiceConnection = new ServiceConnection() {
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||||
|
// 1. 安全获取服务实例(避免强转失败+服务未就绪)
|
||||||
|
if (!(service instanceof MainService.LocalBinder)) {
|
||||||
|
LogUtils.e(TAG, "服务绑定失败:Binder类型不匹配(非MainService.LocalBinder)");
|
||||||
|
isServiceBound.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
MainService.LocalBinder binder = (MainService.LocalBinder) service;
|
||||||
|
mMainService = binder.getService();
|
||||||
|
// 2. 标记服务绑定成功(原子操作,确保多线程可见)
|
||||||
|
isServiceBound.set(true);
|
||||||
|
LogUtils.d(TAG, "MainService绑定成功,开始同步数据+初始化Adapter");
|
||||||
|
|
||||||
|
// 3. 同步服务数据到本地缓存(核心:先同步数据,再初始化Adapter)
|
||||||
|
syncDataFromMainService();
|
||||||
|
// 4. 注册GPS监听(确保监听在Adapter前初始化,数据不丢失)
|
||||||
|
registerGpsListener();
|
||||||
|
// 5. 初始化Adapter(传入本地缓存+服务实例,数据非空)
|
||||||
|
initPositionAdapter();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.d(TAG, "服务绑定后初始化失败:" + e.getMessage());
|
||||||
|
isServiceBound.set(false);
|
||||||
|
mMainService = null;
|
||||||
|
showToast("服务初始化失败,无法加载数据");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
LogUtils.w(TAG, "MainService断开连接,清空引用+标记状态");
|
||||||
|
// 1. 清空服务引用+标记绑定状态
|
||||||
|
mMainService = null;
|
||||||
|
isServiceBound.set(false);
|
||||||
|
// 2. 标记Adapter未初始化(下次绑定需重新初始化)
|
||||||
|
isAdapterInited.set(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Activity getActivity() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTag() {
|
||||||
|
return TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_location);
|
||||||
|
|
||||||
|
mToolbar = findViewById(R.id.toolbar);
|
||||||
|
setSupportActionBar(mToolbar);
|
||||||
|
mToolbar.setSubtitle(getTag());
|
||||||
|
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||||
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
LogUtils.d(TAG, "【导航栏】点击返回");
|
||||||
|
startActivity(new Intent(LocationActivity.this, MainActivity.class));
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. 初始化视图(优先执行,避免Adapter初始化时视图为空)
|
||||||
|
initView();
|
||||||
|
// 2. 初始化GPS监听(提前创建,避免绑定服务后空指针)
|
||||||
|
initGpsUpdateListener();
|
||||||
|
// 3. 绑定MainService(最后执行,确保视图/监听已就绪)
|
||||||
|
bindMainService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化视图(RecyclerView)- 确保视图先于Adapter初始化
|
||||||
|
*/
|
||||||
|
private void initView() {
|
||||||
|
mRvPosition = (RecyclerView) findViewById(R.id.rv_position_list);
|
||||||
|
// 1. 显式设置布局管理器(避免Adapter设置时无布局管理器崩溃)
|
||||||
|
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||||
|
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
|
||||||
|
mRvPosition.setLayoutManager(layoutManager);
|
||||||
|
// 2. 初始化本地缓存(避免首次加载时缓存为空)
|
||||||
|
mLocalPosCache.clear();
|
||||||
|
LogUtils.d(TAG, "视图初始化完成(布局管理器+本地缓存已就绪)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定MainService(Java 7 显式Intent,强化绑定安全性)
|
||||||
|
*/
|
||||||
|
private void bindMainService() {
|
||||||
|
// 1. 避免重复绑定(快速重建Activity时防止多绑定)
|
||||||
|
if (isServiceBound.get()) {
|
||||||
|
LogUtils.w(TAG, "无需重复绑定:MainService已绑定");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent serviceIntent = new Intent(this, MainService.class);
|
||||||
|
// 2. 绑定服务(BIND_AUTO_CREATE:服务不存在时自动创建,增加绑定成功率)
|
||||||
|
boolean bindSuccess = bindService(serviceIntent, mServiceConnection, BIND_AUTO_CREATE);
|
||||||
|
if (!bindSuccess) {
|
||||||
|
LogUtils.e(TAG, "发起MainService绑定请求失败(服务未找到/系统限制)");
|
||||||
|
showToast("服务绑定失败,无法加载位置数据");
|
||||||
|
} else {
|
||||||
|
LogUtils.d(TAG, "MainService绑定请求已发起");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从MainService同步数据到本地缓存(核心解决Adapter空数据问题)
|
||||||
|
* 作用:1. 服务数据优先同步到本地,Adapter基于本地缓存初始化
|
||||||
|
* 2. 避免服务数据更新时直接操作Adapter,通过缓存中转
|
||||||
|
*/
|
||||||
|
private void syncDataFromMainService() {
|
||||||
|
// 1. 安全校验(服务未绑定/服务空,用本地缓存兜底)
|
||||||
|
if (!isServiceBound.get() || mMainService == null) {
|
||||||
|
LogUtils.w(TAG, "同步数据:服务未就绪,使用本地缓存(当前缓存量=" + mLocalPosCache.size() + ")");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. 从服务获取最新位置数据(同步操作,确保数据拿到后再返回)
|
||||||
|
ArrayList<PositionModel> servicePosList = mMainService.getPositionList();
|
||||||
|
// 3. 同步到本地缓存(清空旧数据+添加新数据,避免重复)
|
||||||
|
synchronized (mLocalPosCache) { // 加锁避免多线程操作缓存冲突
|
||||||
|
mLocalPosCache.clear();
|
||||||
|
if (servicePosList != null && !servicePosList.isEmpty()) {
|
||||||
|
mLocalPosCache.addAll(servicePosList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogUtils.d(TAG, "数据同步完成:服务位置数=" + (servicePosList == null ? 0 : servicePosList.size())
|
||||||
|
+ ",本地缓存数=" + mLocalPosCache.size());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.d(TAG, "同步服务数据失败:" + e.getMessage());
|
||||||
|
// 异常时保留本地缓存,避免Adapter无数据
|
||||||
|
LogUtils.w(TAG, "同步失败,使用本地缓存兜底(缓存量=" + mLocalPosCache.size() + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化PositionAdapter(核心优化:基于本地缓存初始化,避免空数据)
|
||||||
|
*/
|
||||||
|
private void initPositionAdapter() {
|
||||||
|
// 1. 多重安全校验(避免销毁后初始化/重复初始化/依赖未就绪)
|
||||||
|
if (isAdapterInited.get() || !isServiceBound.get() || mMainService == null || mRvPosition == null) {
|
||||||
|
LogUtils.w(TAG, "Adapter初始化跳过:"
|
||||||
|
+ "已初始化=" + isAdapterInited.get()
|
||||||
|
+ ",服务绑定=" + isServiceBound.get()
|
||||||
|
+ ",视图就绪=" + (mRvPosition != null));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. 基于本地缓存初始化Adapter(缓存已同步服务数据,非空)
|
||||||
|
mPositionAdapter = new PositionAdapter(this, mLocalPosCache, mMainService);
|
||||||
|
|
||||||
|
// 3. 设置删除回调(删除时同步服务+本地缓存+Adapter)
|
||||||
|
mPositionAdapter.setOnDeleteClickListener(new PositionAdapter.OnDeleteClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onDeleteClick(final int position) {
|
||||||
|
YesNoAlertDialog.show(LocationActivity.this, "删除位置提示", "是否删除此项锚点位置?", new YesNoAlertDialog.OnDialogResultListener(){
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onYes() {
|
||||||
|
// 安全校验(索引有效+服务绑定+缓存非空)
|
||||||
|
if (position < 0 || position >= mLocalPosCache.size() || !isServiceBound.get() || mMainService == null) {
|
||||||
|
LogUtils.w(TAG, "删除位置失败:索引无效/服务未就绪(索引=" + position + ",缓存量=" + mLocalPosCache.size() + ")");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PositionModel deletePos = mLocalPosCache.get(position);
|
||||||
|
if (deletePos != null && !deletePos.getPositionId().isEmpty()) {
|
||||||
|
// 步骤1:调用服务删除(确保服务数据一致性)
|
||||||
|
mMainService.removePosition(deletePos.getPositionId());
|
||||||
|
// 步骤2:删除本地缓存(确保缓存与服务同步)
|
||||||
|
synchronized (mLocalPosCache) {
|
||||||
|
mLocalPosCache.remove(position);
|
||||||
|
}
|
||||||
|
// 步骤3:通知Adapter刷新(基于缓存操作,避免空数据)
|
||||||
|
mPositionAdapter.notifyItemRemoved(position);
|
||||||
|
showToast("删除位置成功:" + deletePos.getMemo());
|
||||||
|
LogUtils.d(TAG, "删除位置完成:ID=" + deletePos.getPositionId() + "(服务+缓存已同步)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 设置保存回调(保存时同步服务+本地缓存+Adapter)
|
||||||
|
mPositionAdapter.setOnSavePositionClickListener(new PositionAdapter.OnSavePositionClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onSavePositionClick(int position, PositionModel updatedPos) {
|
||||||
|
// 安全校验(索引有效+服务绑定+数据非空)
|
||||||
|
if (!isServiceBound.get() || mMainService == null
|
||||||
|
|| position < 0 || position >= mLocalPosCache.size() || updatedPos == null) {
|
||||||
|
LogUtils.w(TAG, "保存位置失败:服务未就绪/索引无效/数据空");
|
||||||
|
showToast("服务未就绪,保存失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤1:调用服务更新(确保服务数据一致性)
|
||||||
|
mMainService.updatePosition(updatedPos);
|
||||||
|
// 步骤2:更新本地缓存(确保缓存与服务同步)
|
||||||
|
synchronized (mLocalPosCache) {
|
||||||
|
mLocalPosCache.set(position, updatedPos);
|
||||||
|
}
|
||||||
|
// 步骤3:通知Adapter刷新(基于缓存操作,避免空数据)
|
||||||
|
mPositionAdapter.notifyItemChanged(position);
|
||||||
|
showToast("保存位置成功:" + updatedPos.getMemo());
|
||||||
|
LogUtils.d(TAG, "保存位置完成:ID=" + updatedPos.getPositionId() + "(服务+缓存已同步)");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. 设置Adapter到RecyclerView(最后一步,确保Adapter已配置完成)
|
||||||
|
mRvPosition.setAdapter(mPositionAdapter);
|
||||||
|
// 6. 标记Adapter已初始化(避免重复初始化)
|
||||||
|
isAdapterInited.set(true);
|
||||||
|
LogUtils.d(TAG, "PositionAdapter初始化完成(基于本地缓存,数据量=" + mLocalPosCache.size() + ")");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.d(TAG, "Adapter初始化失败:" + e.getMessage());
|
||||||
|
isAdapterInited.set(false);
|
||||||
|
mPositionAdapter = null;
|
||||||
|
showToast("位置列表初始化失败,请重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示Toast(Java 7 显式Toast.makeText,避免空Context)
|
||||||
|
*/
|
||||||
|
private void showToast(String content) {
|
||||||
|
if (isFinishing() || isDestroyed()) { // 避免Activity销毁后弹Toast崩溃
|
||||||
|
LogUtils.w(TAG, "Activity已销毁,跳过Toast:" + content);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Toast.makeText(this, content, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- 页面交互(新增位置逻辑保留,适配GPS数据) ----------------------
|
||||||
|
/**
|
||||||
|
* 新增位置(调用服务addPosition(),可选:用当前GPS位置初始化新位置)
|
||||||
|
*/
|
||||||
|
public void addNewPosition(View view) {
|
||||||
|
// 1. 隐藏软键盘(避免软键盘遮挡操作)
|
||||||
|
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
if (imm != null && getCurrentFocus() != null) {
|
||||||
|
imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 安全校验(服务未绑定,不允许新增)
|
||||||
|
if (!isServiceBound.get() || mMainService == null) {
|
||||||
|
LogUtils.w(TAG, "新增位置失败:MainService未绑定");
|
||||||
|
showToast("服务未就绪,无法新增位置");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 创建新位置模型(优化:优先用当前GPS位置初始化,无则用默认值)
|
||||||
|
PositionModel newPos = new PositionModel();
|
||||||
|
newPos.setPositionId(PositionModel.genPositionId()); // 生成唯一ID(需PositionModel实现)
|
||||||
|
if (mCurrentGpsPos != null) {
|
||||||
|
newPos.setLongitude(mCurrentGpsPos.getLongitude());
|
||||||
|
newPos.setLatitude(mCurrentGpsPos.getLatitude());
|
||||||
|
newPos.setMemo("当前GPS位置(可编辑)");
|
||||||
|
} else {
|
||||||
|
newPos.setLongitude(116.404267); // 北京经度(默认值)
|
||||||
|
newPos.setLatitude(39.915119); // 北京纬度(默认值)
|
||||||
|
newPos.setMemo("默认位置(可编辑备注)");
|
||||||
|
}
|
||||||
|
newPos.setIsSimpleView(true); // 默认简单视图
|
||||||
|
newPos.setIsEnableRealPositionDistance(true); // 启用距离计算(依赖GPS)
|
||||||
|
|
||||||
|
// 4. 调用服务新增+同步本地缓存(确保缓存与服务一致)
|
||||||
|
mMainService.addPosition(newPos);
|
||||||
|
synchronized (mLocalPosCache) {
|
||||||
|
mLocalPosCache.add(newPos);
|
||||||
|
}
|
||||||
|
LogUtils.d(TAG, "通过服务新增位置:ID=" + newPos.getPositionId() + ",纬度=" + newPos.getLatitude() + "(缓存已同步)");
|
||||||
|
|
||||||
|
// 5. 刷新Adapter(基于缓存操作,确保数据立即显示)
|
||||||
|
if (isAdapterInited.get() && mPositionAdapter != null) {
|
||||||
|
mPositionAdapter.notifyItemInserted(mLocalPosCache.size() - 1);
|
||||||
|
}
|
||||||
|
showToast("新增位置成功(已启用GPS距离计算)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- 新增:GPS监听初始化+注册/反注册(核心适配逻辑) ----------------------
|
||||||
|
/**
|
||||||
|
* 初始化GPS监听:实现MainService.GpsUpdateListener,接收实时GPS数据
|
||||||
|
*/
|
||||||
|
private void initGpsUpdateListener() {
|
||||||
|
LogUtils.d(TAG, "initGpsUpdateListener()");
|
||||||
|
mGpsUpdateListener = new MainService.GpsUpdateListener() {
|
||||||
|
@Override
|
||||||
|
public void onGpsPositionUpdated(PositionModel currentGpsPos) {
|
||||||
|
if (currentGpsPos == null || isFinishing() || isDestroyed()) {
|
||||||
|
LogUtils.w(TAG, "GPS位置更新:数据为空或Activity已销毁");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 缓存当前GPS位置(供页面其他逻辑使用)
|
||||||
|
mCurrentGpsPos = currentGpsPos;
|
||||||
|
LogUtils.d(TAG, String.format("收到GPS更新:纬度=%.4f,经度=%.4f"
|
||||||
|
, currentGpsPos.getLatitude(), currentGpsPos.getLongitude()));
|
||||||
|
// 安全更新UI(避免Activity销毁后操作视图崩溃)
|
||||||
|
((TextView)findViewById(R.id.tv_latitude)).setText(String.format("当前纬度:%f", currentGpsPos.getLatitude()));
|
||||||
|
((TextView)findViewById(R.id.tv_longitude)).setText(String.format("当前经度:%f", currentGpsPos.getLongitude()));
|
||||||
|
// 设置格式化后的时间字符串
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
|
||||||
|
String timeStr = sdf.format(new Date(currentTime));
|
||||||
|
// 设置当前时间
|
||||||
|
((TextView)findViewById(R.id.tv_timenow)).setText("现在时间:" + timeStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onGpsStatusChanged(String status) {
|
||||||
|
if (status == null || isFinishing() || isDestroyed()) return;
|
||||||
|
LogUtils.d(TAG, "GPS状态变化:" + status);
|
||||||
|
if (status.contains("未开启") || status.contains("权限") || status.contains("失败")) {
|
||||||
|
ToastUtils.show("GPS提示:" + status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册GPS监听:调用MainService的PUBLIC方法,绑定监听
|
||||||
|
*/
|
||||||
|
private void registerGpsListener() {
|
||||||
|
// 安全校验(避免Activity销毁/服务未绑定/监听为空时注册)
|
||||||
|
if (isFinishing() || isDestroyed() || !isServiceBound.get() || mMainService == null || mGpsUpdateListener == null) {
|
||||||
|
LogUtils.w(TAG, "GPS监听注册跳过:Activity状态异常/依赖未就绪");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mMainService.registerGpsUpdateListener(mGpsUpdateListener);
|
||||||
|
LogUtils.d(TAG, "GPS监听已注册");
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.d(TAG, "GPS监听注册失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反注册GPS监听:调用MainService的PUBLIC方法,解绑监听(核心防内存泄漏+数据异常)
|
||||||
|
*/
|
||||||
|
private void unregisterGpsListener() {
|
||||||
|
// 避免Activity销毁后调用服务方法(防止空指针/服务已解绑)
|
||||||
|
if (mMainService == null || mGpsUpdateListener == null) {
|
||||||
|
LogUtils.w(TAG, "GPS监听反注册跳过:服务/监听未初始化");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mMainService.unregisterGpsUpdateListener(mGpsUpdateListener);
|
||||||
|
LogUtils.d(TAG, "GPS监听已反注册");
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.d(TAG, "GPS监听反注册失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面可见时同步数据(解决快速切回时数据未更新问题)
|
||||||
|
* 场景:快速关闭再打开Activity,服务已绑定但数据未重新同步
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
// 1. 服务已绑定但Adapter未初始化:重新同步数据+初始化Adapter
|
||||||
|
if (isServiceBound.get() && mMainService != null && !isAdapterInited.get()) {
|
||||||
|
LogUtils.d(TAG, "onResume:服务已绑定但Adapter未初始化,重新同步数据");
|
||||||
|
syncDataFromMainService();
|
||||||
|
initPositionAdapter();
|
||||||
|
} else if (isServiceBound.get() && mMainService != null && isAdapterInited.get() && mPositionAdapter != null) {
|
||||||
|
syncDataFromMainService();
|
||||||
|
mPositionAdapter.notifyDataSetChanged();
|
||||||
|
LogUtils.d(TAG, "onResume:刷新位置数据(与服务同步)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面不可见时暂停操作(避免后台操作导致数据异常)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
// 避免后台时仍执行UI刷新(如GPS更新触发的视图操作)
|
||||||
|
LogUtils.d(TAG, "onPause:页面不可见,暂停UI相关操作");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
LogUtils.d(TAG, "onDestroy:开始释放资源");
|
||||||
|
|
||||||
|
// 1. 反注册GPS监听(优先执行,避免服务持有Activity引用导致内存泄漏)
|
||||||
|
unregisterGpsListener();
|
||||||
|
|
||||||
|
// 2. 释放Adapter资源(反注册可能的监听,避免内存泄漏)
|
||||||
|
if (mPositionAdapter != null) {
|
||||||
|
mPositionAdapter.release();
|
||||||
|
mPositionAdapter = null; // 清空引用,帮助GC回收
|
||||||
|
LogUtils.d(TAG, "Adapter资源已释放");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 解绑MainService(最后执行,确保其他资源已释放)
|
||||||
|
if (isServiceBound.get()) {
|
||||||
|
try {
|
||||||
|
unbindService(mServiceConnection);
|
||||||
|
LogUtils.d(TAG, "MainService解绑完成");
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// 捕获“服务未绑定”异常(快速开关时可能出现,避免崩溃)
|
||||||
|
LogUtils.d(TAG, "解绑MainService失败:服务未绑定(可能已提前解绑)");
|
||||||
|
}
|
||||||
|
// 重置绑定状态+服务引用
|
||||||
|
isServiceBound.set(false);
|
||||||
|
mMainService = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 清空本地缓存+GPS引用(帮助GC回收)
|
||||||
|
synchronized (mLocalPosCache) {
|
||||||
|
mLocalPosCache.clear();
|
||||||
|
}
|
||||||
|
mCurrentGpsPos = null;
|
||||||
|
mGpsUpdateListener = null;
|
||||||
|
isAdapterInited.set(false);
|
||||||
|
LogUtils.d(TAG, "所有资源释放完成(onDestroy执行结束)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- 移除重复定义:LocalBinder 统一在 MainService 中定义 ----------------------
|
||||||
|
// 说明:原LocationActivity中的LocalBinder是重复定义(MainService已实现),会导致类型强转失败
|
||||||
|
// 此处删除该类,确保Activity绑定服务时强转的是MainService中的LocalBinder
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package cc.winboll.studio.positions.activities;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
|
import cc.winboll.studio.positions.R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
|
* @Date 2025/12/07 23:29
|
||||||
|
* @Describe 应用设置活动窗口
|
||||||
|
*/
|
||||||
|
public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||||
|
|
||||||
|
public static final String TAG = "SettingsActivity";
|
||||||
|
|
||||||
|
private Toolbar mToolbar;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Activity getActivity() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTag() {
|
||||||
|
return TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_settings);
|
||||||
|
|
||||||
|
mToolbar = findViewById(R.id.toolbar);
|
||||||
|
setSupportActionBar(mToolbar);
|
||||||
|
mToolbar.setSubtitle(getTag());
|
||||||
|
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||||
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
LogUtils.d(TAG, "【导航栏】点击返回");
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package cc.winboll.studio.positions.activities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
|
* @Date 2025/09/29 00:11
|
||||||
|
* @Describe WinBoLL 窗口基础类
|
||||||
|
*/
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||||
|
import cc.winboll.studio.libaes.models.AESThemeBean;
|
||||||
|
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||||
|
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
public class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||||
|
|
||||||
|
public static final String TAG = "WinBoLLActivity";
|
||||||
|
|
||||||
|
protected volatile AESThemeBean.ThemeType mThemeType;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Activity getActivity() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTag() {
|
||||||
|
return TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
mThemeType = getThemeType();
|
||||||
|
setThemeStyle();
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
AESThemeBean.ThemeType getThemeType() {
|
||||||
|
return AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setThemeStyle() {
|
||||||
|
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
LogUtils.d(TAG, String.format("onResume %s", getTag()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
/*if (item.getItemId() == R.id.item_log) {
|
||||||
|
WinBoLLActivityManager.getInstance().startLogActivity(this);
|
||||||
|
return true;
|
||||||
|
} else if (item.getItemId() == R.id.item_home) {
|
||||||
|
startActivity(new Intent(this, MainActivity.class));
|
||||||
|
return true;
|
||||||
|
}*/
|
||||||
|
// 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostCreate(Bundle savedInstanceState) {
|
||||||
|
super.onPostCreate(savedInstanceState);
|
||||||
|
WinBoLLActivityManager.getInstance().add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
WinBoLLActivityManager.getInstance().registeRemove(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,639 @@
|
|||||||
|
package cc.winboll.studio.positions.adapters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Describe 位置数据适配器(修复视图复用资源加载,支持滚动后重新绑定数据,Java 7语法适配)
|
||||||
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @CreateTime 2025-09-29 20:25:00
|
||||||
|
* @EditTime 2026-03-31 23:14:55
|
||||||
|
*/
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.RadioGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.positions.R;
|
||||||
|
import cc.winboll.studio.positions.models.PositionModel;
|
||||||
|
import cc.winboll.studio.positions.models.PositionTaskModel;
|
||||||
|
import cc.winboll.studio.positions.services.MainService;
|
||||||
|
import cc.winboll.studio.positions.views.PositionTaskListView;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class PositionAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
|
implements MainService.TaskUpdateListener {
|
||||||
|
|
||||||
|
public static final String TAG = "PositionAdapter";
|
||||||
|
|
||||||
|
// 视图类型常量
|
||||||
|
private static final int VIEW_TYPE_SIMPLE = 0;
|
||||||
|
private static final int VIEW_TYPE_EDIT = 1;
|
||||||
|
|
||||||
|
// 默认配置常量
|
||||||
|
private static final String DEFAULT_MEMO = "无备注";
|
||||||
|
private static final String DEFAULT_TASK_DESC = "新任务";
|
||||||
|
private static final int DEFAULT_TASK_DISTANCE = 50;
|
||||||
|
private static final String DISTANCE_FORMAT = "实时距离:%.1f 米";
|
||||||
|
private static final String DISTANCE_DISABLED = "实时距离:未启用";
|
||||||
|
private static final String DISTANCE_ERROR = "实时距离:计算失败";
|
||||||
|
|
||||||
|
// 核心成员变量
|
||||||
|
private final Context mContext;
|
||||||
|
private final ArrayList<PositionModel> mCachedPositionList;
|
||||||
|
private final WeakReference<MainService> mMainServiceRef;
|
||||||
|
private final ConcurrentHashMap<String, PositionTaskListView> mSimpleTaskViewMap;
|
||||||
|
private final ConcurrentHashMap<String, PositionTaskListView> mEditTaskViewMap;
|
||||||
|
private final ConcurrentHashMap<String, TextView> mPosDistanceViewMap;
|
||||||
|
|
||||||
|
// 回调接口
|
||||||
|
public interface OnDeleteClickListener {
|
||||||
|
void onDeleteClick(int position);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnSavePositionClickListener {
|
||||||
|
void onSavePositionClick(int position, PositionModel updatedPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OnDeleteClickListener mOnDeleteListener;
|
||||||
|
private OnSavePositionClickListener mOnSavePosListener;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 构造函数
|
||||||
|
// =========================================================================
|
||||||
|
public PositionAdapter(Context context, ArrayList<PositionModel> cachedPositionList, MainService mainService) {
|
||||||
|
LogUtils.d(TAG, "PositionAdapter 构造函数开始,context=" + context
|
||||||
|
+ ",cachedPositionList=" + (cachedPositionList != null ? cachedPositionList.size() : 0)
|
||||||
|
+ ",mainService=" + mainService);
|
||||||
|
|
||||||
|
this.mContext = context;
|
||||||
|
this.mCachedPositionList = (cachedPositionList != null) ? cachedPositionList : new ArrayList<PositionModel>();
|
||||||
|
this.mMainServiceRef = new WeakReference<MainService>(mainService);
|
||||||
|
this.mSimpleTaskViewMap = new ConcurrentHashMap<String, PositionTaskListView>();
|
||||||
|
this.mEditTaskViewMap = new ConcurrentHashMap<String, PositionTaskListView>();
|
||||||
|
this.mPosDistanceViewMap = new ConcurrentHashMap<String, TextView>();
|
||||||
|
|
||||||
|
if (mainService != null) {
|
||||||
|
mainService.registerTaskUpdateListener(this);
|
||||||
|
LogUtils.d(TAG, "已注册 MainService 任务监听");
|
||||||
|
} else {
|
||||||
|
LogUtils.w(TAG, "构造函数:MainService 为空,无法初始化任务视图");
|
||||||
|
}
|
||||||
|
|
||||||
|
LogUtils.d(TAG, "PositionAdapter 初始化完成,位置数量=" + mCachedPositionList.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// RecyclerView 核心方法
|
||||||
|
// =========================================================================
|
||||||
|
@Override
|
||||||
|
public int getItemViewType(int position) {
|
||||||
|
PositionModel posModel = getPositionByIndex(position);
|
||||||
|
return (posModel != null && posModel.isSimpleView()) ? VIEW_TYPE_SIMPLE : VIEW_TYPE_EDIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
|
LogUtils.d(TAG, "onCreateViewHolder viewType=" + viewType);
|
||||||
|
LayoutInflater inflater = LayoutInflater.from(mContext);
|
||||||
|
if (viewType == VIEW_TYPE_SIMPLE) {
|
||||||
|
View simpleView = inflater.inflate(R.layout.item_position_simple, parent, false);
|
||||||
|
return new SimpleViewHolder(simpleView);
|
||||||
|
} else {
|
||||||
|
View editView = inflater.inflate(R.layout.item_position_edit, parent, false);
|
||||||
|
return new EditViewHolder(editView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||||
|
LogUtils.d(TAG, "onBindViewHolder position=" + position);
|
||||||
|
PositionModel posModel = getPositionByIndex(position);
|
||||||
|
if (posModel == null) {
|
||||||
|
LogUtils.w(TAG, "onBindViewHolder:位置模型为空,索引=" + position);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String posId = posModel.getPositionId();
|
||||||
|
MainService mainService = mMainServiceRef.get();
|
||||||
|
|
||||||
|
if (holder instanceof SimpleViewHolder) {
|
||||||
|
LogUtils.d(TAG, "绑定 SimpleViewHolder,posId=" + posId);
|
||||||
|
SimpleViewHolder simpleHolder = (SimpleViewHolder) holder;
|
||||||
|
bindSimplePositionData(simpleHolder, posModel);
|
||||||
|
initAndBindSimpleTaskView(simpleHolder.ptlvSimpleTasks, posId, mainService);
|
||||||
|
mSimpleTaskViewMap.put(posId, simpleHolder.ptlvSimpleTasks);
|
||||||
|
mPosDistanceViewMap.put(posId, simpleHolder.tvSimpleDistance);
|
||||||
|
|
||||||
|
} else if (holder instanceof EditViewHolder) {
|
||||||
|
LogUtils.d(TAG, "绑定 EditViewHolder,posId=" + posId);
|
||||||
|
EditViewHolder editHolder = (EditViewHolder) holder;
|
||||||
|
bindEditPositionData(editHolder, posModel, position);
|
||||||
|
initAndBindEditTaskView(editHolder.ptlvEditTasks, posId, mainService, editHolder.btnAddTask);
|
||||||
|
mEditTaskViewMap.put(posId, editHolder.ptlvEditTasks);
|
||||||
|
mPosDistanceViewMap.put(posId, editHolder.tvEditDistance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) {
|
||||||
|
super.onViewDetachedFromWindow(holder);
|
||||||
|
PositionModel posModel = getPositionByIndex(holder.getAdapterPosition());
|
||||||
|
if (posModel == null || TextUtils.isEmpty(posModel.getPositionId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String posId = posModel.getPositionId();
|
||||||
|
|
||||||
|
if (mPosDistanceViewMap.containsKey(posId)) {
|
||||||
|
TextView distanceView = mPosDistanceViewMap.get(posId);
|
||||||
|
if (distanceView == null || !distanceView.isAttachedToWindow()) {
|
||||||
|
mPosDistanceViewMap.remove(posId);
|
||||||
|
LogUtils.d(TAG, "视图脱离:移除无效距离控件缓存 posId=" + posId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (holder instanceof SimpleViewHolder && mSimpleTaskViewMap.containsKey(posId)) {
|
||||||
|
PositionTaskListView taskView = mSimpleTaskViewMap.get(posId);
|
||||||
|
if (taskView == null || !taskView.isAttachedToWindow()) {
|
||||||
|
mSimpleTaskViewMap.remove(posId);
|
||||||
|
LogUtils.d(TAG, "视图脱离:移除无效简单任务视图缓存 posId=" + posId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (holder instanceof EditViewHolder && mEditTaskViewMap.containsKey(posId)) {
|
||||||
|
PositionTaskListView taskView = mEditTaskViewMap.get(posId);
|
||||||
|
if (taskView == null || !taskView.isAttachedToWindow()) {
|
||||||
|
mEditTaskViewMap.remove(posId);
|
||||||
|
LogUtils.d(TAG, "视图脱离:移除无效编辑任务视图缓存 posId=" + posId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return mCachedPositionList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 位置数据绑定
|
||||||
|
// =========================================================================
|
||||||
|
private void bindSimplePositionData(SimpleViewHolder holder, final PositionModel posModel) {
|
||||||
|
LogUtils.d(TAG, "bindSimplePositionData posId=" + posModel.getPositionId());
|
||||||
|
holder.tvSimpleLon.setText(String.format("经度:%.6f", posModel.getLongitude()));
|
||||||
|
holder.tvSimpleLat.setText(String.format("纬度:%.6f", posModel.getLatitude()));
|
||||||
|
|
||||||
|
String memo = posModel.getMemo();
|
||||||
|
holder.tvSimpleMemo.setText("备注:" + (TextUtils.isEmpty(memo) ? DEFAULT_MEMO : memo));
|
||||||
|
updateDistanceDisplay(holder.tvSimpleDistance, posModel);
|
||||||
|
|
||||||
|
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
posModel.setIsSimpleView(false);
|
||||||
|
notifyItemChanged(getPositionIndexById(posModel.getPositionId()));
|
||||||
|
LogUtils.d(TAG, "简单视图点击:切换编辑模式 posId=" + posModel.getPositionId());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindEditPositionData(final EditViewHolder holder, final PositionModel posModel, final int position) {
|
||||||
|
LogUtils.d(TAG, "bindEditPositionData posId=" + posModel.getPositionId() + " position=" + position);
|
||||||
|
final String posId = posModel.getPositionId();
|
||||||
|
|
||||||
|
holder.tvEditLon.setText(String.format("经度:%.6f", posModel.getLongitude()));
|
||||||
|
holder.tvEditLat.setText(String.format("纬度:%.6f", posModel.getLatitude()));
|
||||||
|
|
||||||
|
String memo = posModel.getMemo();
|
||||||
|
if (!TextUtils.isEmpty(memo)) {
|
||||||
|
holder.etEditMemo.setText(memo);
|
||||||
|
holder.etEditMemo.setSelection(memo.length());
|
||||||
|
} else {
|
||||||
|
holder.etEditMemo.setText("");
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDistanceDisplay(holder.tvEditDistance, posModel);
|
||||||
|
|
||||||
|
holder.rgDistanceSwitch.check(posModel.isEnableRealPositionDistance()
|
||||||
|
? R.id.rb_distance_enable
|
||||||
|
: R.id.rb_distance_disable);
|
||||||
|
|
||||||
|
holder.btnCancel.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
posModel.setIsSimpleView(true);
|
||||||
|
notifyItemChanged(position);
|
||||||
|
hideSoftKeyboard(v);
|
||||||
|
LogUtils.d(TAG, "取消编辑:切换简单视图 posId=" + posId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
holder.btnDelete.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
if (mOnDeleteListener != null) {
|
||||||
|
mOnDeleteListener.onDeleteClick(position);
|
||||||
|
}
|
||||||
|
hideSoftKeyboard(v);
|
||||||
|
LogUtils.d(TAG, "触发删除位置 posId=" + posId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
holder.btnSave.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
String newMemo = holder.etEditMemo.getText().toString().trim();
|
||||||
|
boolean isDistanceEnable = (holder.rgDistanceSwitch.getCheckedRadioButtonId() == R.id.rb_distance_enable);
|
||||||
|
|
||||||
|
PositionModel updatedPos = new PositionModel();
|
||||||
|
updatedPos.setPositionId(posId);
|
||||||
|
updatedPos.setLongitude(posModel.getLongitude());
|
||||||
|
updatedPos.setLatitude(posModel.getLatitude());
|
||||||
|
updatedPos.setMemo(newMemo);
|
||||||
|
updatedPos.setIsEnableRealPositionDistance(isDistanceEnable);
|
||||||
|
updatedPos.setIsSimpleView(true);
|
||||||
|
|
||||||
|
if (mOnSavePosListener != null) {
|
||||||
|
mOnSavePosListener.onSavePositionClick(position, updatedPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
posModel.setMemo(newMemo);
|
||||||
|
posModel.setIsEnableRealPositionDistance(isDistanceEnable);
|
||||||
|
posModel.setIsSimpleView(true);
|
||||||
|
notifyItemChanged(position);
|
||||||
|
hideSoftKeyboard(v);
|
||||||
|
|
||||||
|
LogUtils.d(TAG, "保存位置 posId=" + posId + " 新备注=" + newMemo + " 距离启用=" + isDistanceEnable);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// PositionTaskListView 集成
|
||||||
|
// =========================================================================
|
||||||
|
private void initAndBindSimpleTaskView(PositionTaskListView taskView, String posId, MainService mainService) {
|
||||||
|
LogUtils.d(TAG, "initAndBindSimpleTaskView posId=" + posId);
|
||||||
|
if (taskView == null || TextUtils.isEmpty(posId) || mainService == null) {
|
||||||
|
LogUtils.w(TAG, "初始化简单任务视图失败:参数无效");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
taskView.init(mainService, posId);
|
||||||
|
taskView.setViewStatus(PositionTaskListView.VIEW_MODE_SIMPLE);
|
||||||
|
taskView.syncTasksFromMainService();
|
||||||
|
|
||||||
|
taskView.setOnTaskUpdatedListener(new PositionTaskListView.OnTaskUpdatedListener() {
|
||||||
|
@Override
|
||||||
|
public void onTaskUpdated(String positionId, ArrayList updatedTasks) {
|
||||||
|
LogUtils.d(TAG, "简单模式任务更新 posId=" + positionId + " 任务数=" + updatedTasks.size());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initAndBindEditTaskView(final PositionTaskListView taskView, final String posId,
|
||||||
|
MainService mainService, Button btnAddTask) {
|
||||||
|
LogUtils.d(TAG, "initAndBindEditTaskView posId=" + posId);
|
||||||
|
if (taskView == null || TextUtils.isEmpty(posId) || mainService == null || btnAddTask == null) {
|
||||||
|
LogUtils.w(TAG, "初始化编辑任务视图失败:参数无效");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
taskView.init(mainService, posId);
|
||||||
|
taskView.setViewStatus(PositionTaskListView.VIEW_MODE_EDIT);
|
||||||
|
taskView.syncTasksFromMainService();
|
||||||
|
|
||||||
|
btnAddTask.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
PositionTaskModel newTask = new PositionTaskModel();
|
||||||
|
newTask.setTaskId(PositionTaskModel.genTaskId());
|
||||||
|
newTask.setPositionId(posId);
|
||||||
|
newTask.setTaskDescription(DEFAULT_TASK_DESC);
|
||||||
|
newTask.setDiscussDistance(DEFAULT_TASK_DISTANCE);
|
||||||
|
newTask.setIsEnable(true);
|
||||||
|
newTask.setIsBingo(false);
|
||||||
|
|
||||||
|
taskView.addNewTask(newTask);
|
||||||
|
hideSoftKeyboard(v);
|
||||||
|
LogUtils.d(TAG, "新增任务 posId=" + posId + " taskId=" + newTask.getTaskId());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
taskView.setOnTaskUpdatedListener(new PositionTaskListView.OnTaskUpdatedListener() {
|
||||||
|
@Override
|
||||||
|
public void onTaskUpdated(String positionId, ArrayList updatedTasks) {
|
||||||
|
LogUtils.d(TAG, "编辑模式任务更新 posId=" + positionId + " 任务数=" + updatedTasks.size());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 工具方法
|
||||||
|
// =========================================================================
|
||||||
|
private void updateDistanceDisplay(TextView distanceView, PositionModel posModel) {
|
||||||
|
if (distanceView == null || posModel == null) {
|
||||||
|
LogUtils.w(TAG, "updateDistanceDisplay:参数为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!posModel.isEnableRealPositionDistance()) {
|
||||||
|
distanceView.setText(DISTANCE_DISABLED);
|
||||||
|
distanceView.setTextColor(mContext.getResources().getColor(R.color.gray));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double distance = posModel.getRealPositionDistance();
|
||||||
|
if (distance < 0) {
|
||||||
|
distanceView.setText(DISTANCE_ERROR);
|
||||||
|
distanceView.setTextColor(mContext.getResources().getColor(R.color.red));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
distanceView.setText(String.format(DISTANCE_FORMAT, distance));
|
||||||
|
if (distance <= 100) {
|
||||||
|
distanceView.setTextColor(mContext.getResources().getColor(R.color.green));
|
||||||
|
} else if (distance <= 500) {
|
||||||
|
distanceView.setTextColor(mContext.getResources().getColor(R.color.yellow));
|
||||||
|
} else {
|
||||||
|
distanceView.setTextColor(mContext.getResources().getColor(R.color.red));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PositionModel getPositionByIndex(int index) {
|
||||||
|
if (mCachedPositionList == null || index < 0 || index >= mCachedPositionList.size()) {
|
||||||
|
LogUtils.w(TAG, "getPositionByIndex:无效索引 index=" + index);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return mCachedPositionList.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getPositionIndexById(String positionId) {
|
||||||
|
if (TextUtils.isEmpty(positionId) || mCachedPositionList == null || mCachedPositionList.isEmpty()) {
|
||||||
|
LogUtils.w(TAG, "getPositionIndexById:参数无效");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < mCachedPositionList.size(); i++) {
|
||||||
|
PositionModel pos = mCachedPositionList.get(i);
|
||||||
|
if (positionId.equals(pos.getPositionId())) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogUtils.w(TAG, "未找到位置ID=" + positionId);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateSinglePositionDistance(String positionId) {
|
||||||
|
LogUtils.d(TAG, "updateSinglePositionDistance posId=" + positionId);
|
||||||
|
if (TextUtils.isEmpty(positionId) || mPosDistanceViewMap.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MainService mainService = getMainServiceWithRetry(2);
|
||||||
|
if (mainService == null) {
|
||||||
|
LogUtils.e(TAG, "无法获取 MainService");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PositionModel latestPos = null;
|
||||||
|
try {
|
||||||
|
ArrayList servicePosList = mainService.getPositionList();
|
||||||
|
if (servicePosList != null && !servicePosList.isEmpty()) {
|
||||||
|
Iterator iter = servicePosList.iterator();
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
PositionModel pos = (PositionModel) iter.next();
|
||||||
|
if (positionId.equals(pos.getPositionId())) {
|
||||||
|
latestPos = pos;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "获取位置数据异常", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (latestPos == null) {
|
||||||
|
LogUtils.w(TAG, "未找到位置 posId=" + positionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final TextView distanceView = mPosDistanceViewMap.get(positionId);
|
||||||
|
if (distanceView != null && distanceView.isAttachedToWindow()) {
|
||||||
|
final PositionModel finalLatestPos = latestPos;
|
||||||
|
distanceView.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
updateDistanceDisplay(distanceView, finalLatestPos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mPosDistanceViewMap.remove(positionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateAllPositionData(ArrayList<PositionModel> newPosList) {
|
||||||
|
LogUtils.d(TAG, "updateAllPositionData 新数据数量=" + (newPosList != null ? newPosList.size() : 0));
|
||||||
|
if (newPosList == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ArrayList<PositionModel> validPosList = new ArrayList<PositionModel>();
|
||||||
|
for (PositionModel pos : newPosList) {
|
||||||
|
if (TextUtils.isEmpty(pos.getPositionId())
|
||||||
|
|| pos.getLongitude() < -180 || pos.getLongitude() > 180
|
||||||
|
|| pos.getLatitude() < -90 || pos.getLatitude() > 90) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
validPosList.add(pos);
|
||||||
|
}
|
||||||
|
ConcurrentHashMap<String, PositionModel> uniquePosMap = new ConcurrentHashMap<String, PositionModel>();
|
||||||
|
for (PositionModel pos : validPosList) {
|
||||||
|
uniquePosMap.put(pos.getPositionId(), pos);
|
||||||
|
}
|
||||||
|
ArrayList uniquePosList = new ArrayList(uniquePosMap.values());
|
||||||
|
|
||||||
|
mCachedPositionList.clear();
|
||||||
|
mCachedPositionList.addAll(uniquePosList);
|
||||||
|
mPosDistanceViewMap.clear();
|
||||||
|
mSimpleTaskViewMap.clear();
|
||||||
|
mEditTaskViewMap.clear();
|
||||||
|
notifyDataSetChanged();
|
||||||
|
|
||||||
|
LogUtils.d(TAG, "全量更新完成,有效数量=" + uniquePosList.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSoftKeyboard(View view) {
|
||||||
|
if (mContext == null || view == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
if (imm != null) {
|
||||||
|
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MainService getMainServiceWithRetry(int retryCount) {
|
||||||
|
MainService mainService = mMainServiceRef.get();
|
||||||
|
if (mainService != null) {
|
||||||
|
return mainService;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < retryCount; i++) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
mainService = mMainServiceRef.get();
|
||||||
|
if (mainService != null) {
|
||||||
|
LogUtils.d(TAG, "重试获取 MainService 成功,第" + (i + 1) + "次");
|
||||||
|
return mainService;
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LogUtils.e(TAG, "重试被中断", e);
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogUtils.e(TAG, "重试" + retryCount + "次仍未获取到 MainService");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 任务更新监听
|
||||||
|
// =========================================================================
|
||||||
|
@Override
|
||||||
|
public void onTaskUpdated() {
|
||||||
|
LogUtils.d(TAG, "onTaskUpdated:收到服务任务更新");
|
||||||
|
if (!mSimpleTaskViewMap.isEmpty()) {
|
||||||
|
Iterator<ConcurrentHashMap.Entry<String, PositionTaskListView>> iter = mSimpleTaskViewMap.entrySet().iterator();
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
ConcurrentHashMap.Entry<String, PositionTaskListView> entry = iter.next();
|
||||||
|
PositionTaskListView taskView = entry.getValue();
|
||||||
|
if (taskView != null && taskView.isAttachedToWindow()) {
|
||||||
|
taskView.syncTasksFromMainService();
|
||||||
|
} else {
|
||||||
|
iter.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mEditTaskViewMap.isEmpty()) {
|
||||||
|
Iterator<ConcurrentHashMap.Entry<String, PositionTaskListView>> iter = mEditTaskViewMap.entrySet().iterator();
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
ConcurrentHashMap.Entry<String, PositionTaskListView> entry = iter.next();
|
||||||
|
PositionTaskListView taskView = entry.getValue();
|
||||||
|
if (taskView != null && taskView.isAttachedToWindow()) {
|
||||||
|
taskView.syncTasksFromMainService();
|
||||||
|
} else {
|
||||||
|
iter.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 外部回调设置
|
||||||
|
// =========================================================================
|
||||||
|
public void setOnDeleteClickListener(OnDeleteClickListener listener) {
|
||||||
|
LogUtils.d(TAG, "setOnDeleteClickListener listener=" + listener);
|
||||||
|
this.mOnDeleteListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnSavePositionClickListener(OnSavePositionClickListener listener) {
|
||||||
|
LogUtils.d(TAG, "setOnSavePositionClickListener listener=" + listener);
|
||||||
|
this.mOnSavePosListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 资源释放
|
||||||
|
// =========================================================================
|
||||||
|
public void release() {
|
||||||
|
LogUtils.d(TAG, "release:开始释放 Adapter 资源");
|
||||||
|
MainService mainService = mMainServiceRef.get();
|
||||||
|
if (mainService != null) {
|
||||||
|
mainService.unregisterTaskUpdateListener(this);
|
||||||
|
LogUtils.d(TAG, "已反注册任务监听");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mSimpleTaskViewMap.isEmpty()) {
|
||||||
|
Iterator<ConcurrentHashMap.Entry<String, PositionTaskListView>> iter = mSimpleTaskViewMap.entrySet().iterator();
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
PositionTaskListView taskView = iter.next().getValue();
|
||||||
|
if (taskView != null) {
|
||||||
|
taskView.clearData();
|
||||||
|
taskView.setOnTaskUpdatedListener(null);
|
||||||
|
}
|
||||||
|
iter.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mEditTaskViewMap.isEmpty()) {
|
||||||
|
Iterator<ConcurrentHashMap.Entry<String, PositionTaskListView>> iter = mEditTaskViewMap.entrySet().iterator();
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
PositionTaskListView taskView = iter.next().getValue();
|
||||||
|
if (taskView != null) {
|
||||||
|
taskView.clearData();
|
||||||
|
taskView.setOnTaskUpdatedListener(null);
|
||||||
|
}
|
||||||
|
iter.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mPosDistanceViewMap.clear();
|
||||||
|
if (mCachedPositionList != null) {
|
||||||
|
mCachedPositionList.clear();
|
||||||
|
}
|
||||||
|
mOnDeleteListener = null;
|
||||||
|
mOnSavePosListener = null;
|
||||||
|
|
||||||
|
LogUtils.d(TAG, "release:资源释放完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// ViewHolder
|
||||||
|
// =========================================================================
|
||||||
|
public static class SimpleViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
TextView tvSimpleLon;
|
||||||
|
TextView tvSimpleLat;
|
||||||
|
TextView tvSimpleMemo;
|
||||||
|
TextView tvSimpleDistance;
|
||||||
|
PositionTaskListView ptlvSimpleTasks;
|
||||||
|
|
||||||
|
public SimpleViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
tvSimpleLon = (TextView) itemView.findViewById(R.id.tv_simple_longitude);
|
||||||
|
tvSimpleLat = (TextView) itemView.findViewById(R.id.tv_simple_latitude);
|
||||||
|
tvSimpleMemo = (TextView) itemView.findViewById(R.id.tv_simple_memo);
|
||||||
|
tvSimpleDistance = (TextView) itemView.findViewById(R.id.tv_simple_distance);
|
||||||
|
ptlvSimpleTasks = (PositionTaskListView) itemView.findViewById(R.id.ptlv_simple_tasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EditViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
TextView tvEditLon;
|
||||||
|
TextView tvEditLat;
|
||||||
|
EditText etEditMemo;
|
||||||
|
TextView tvEditDistance;
|
||||||
|
RadioGroup rgDistanceSwitch;
|
||||||
|
Button btnCancel;
|
||||||
|
Button btnDelete;
|
||||||
|
Button btnSave;
|
||||||
|
Button btnAddTask;
|
||||||
|
TextView tvTaskCount;
|
||||||
|
PositionTaskListView ptlvEditTasks;
|
||||||
|
|
||||||
|
public EditViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
tvEditLon = (TextView) itemView.findViewById(R.id.tv_edit_longitude);
|
||||||
|
tvEditLat = (TextView) itemView.findViewById(R.id.tv_edit_latitude);
|
||||||
|
etEditMemo = (EditText) itemView.findViewById(R.id.et_edit_memo);
|
||||||
|
tvEditDistance = (TextView) itemView.findViewById(R.id.tv_edit_distance);
|
||||||
|
rgDistanceSwitch = (RadioGroup) itemView.findViewById(R.id.rg_distance_switch);
|
||||||
|
btnCancel = (Button) itemView.findViewById(R.id.btn_edit_cancel);
|
||||||
|
btnDelete = (Button) itemView.findViewById(R.id.btn_edit_delete);
|
||||||
|
btnSave = (Button) itemView.findViewById(R.id.btn_edit_save);
|
||||||
|
btnAddTask = (Button) itemView.findViewById(R.id.btn_add_task);
|
||||||
|
tvTaskCount = (TextView) itemView.findViewById(R.id.tv_task_count);
|
||||||
|
ptlvEditTasks = (PositionTaskListView) itemView.findViewById(R.id.ptlv_edit_tasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 方法说明类(保留不动)
|
||||||
|
// =========================================================================
|
||||||
|
public static class PositionTaskListViewRequiredMethods {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package cc.winboll.studio.positions.models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
|
* @Date 2025/10/01 04:50
|
||||||
|
* @Describe AppConfigsModel
|
||||||
|
*/
|
||||||
|
import cc.winboll.studio.libappbase.BaseBean;
|
||||||
|
import android.util.JsonWriter;
|
||||||
|
import android.util.JsonReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class AppConfigsModel extends BaseBean {
|
||||||
|
|
||||||
|
public static final String TAG = "AppConfigsModel";
|
||||||
|
|
||||||
|
boolean isEnableMainService;
|
||||||
|
|
||||||
|
public AppConfigsModel(boolean isEnableMainService) {
|
||||||
|
this.isEnableMainService = isEnableMainService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppConfigsModel() {
|
||||||
|
this.isEnableMainService = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsEnableMainService(boolean isEnableMainService) {
|
||||||
|
this.isEnableMainService = isEnableMainService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnableMainService() {
|
||||||
|
return isEnableMainService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return AppConfigsModel.class.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON序列化(保存位置数据)
|
||||||
|
@Override
|
||||||
|
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||||
|
super.writeThisToJsonWriter(jsonWriter);
|
||||||
|
jsonWriter.name("isEnableDistanceRefreshService").value(isEnableMainService());
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON反序列化(加载位置数据,校验字段)
|
||||||
|
@Override
|
||||||
|
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||||
|
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
if (name.equals("isEnableDistanceRefreshService")) {
|
||||||
|
setIsEnableMainService(jsonReader.nextBoolean());
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从JSON读取位置数据
|
||||||
|
@Override
|
||||||
|
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||||
|
jsonReader.beginObject();
|
||||||
|
while (jsonReader.hasNext()) {
|
||||||
|
String name = jsonReader.nextName();
|
||||||
|
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||||
|
jsonReader.skipValue(); // 跳过未知字段
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsonReader.endObject();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
package cc.winboll.studio.positions.models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
|
* @Date 2025/09/29 18:57
|
||||||
|
* @Describe 位置数据模型
|
||||||
|
*/
|
||||||
|
import android.util.JsonReader;
|
||||||
|
import android.util.JsonWriter;
|
||||||
|
import cc.winboll.studio.libappbase.BaseBean;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class PositionModel extends BaseBean {
|
||||||
|
|
||||||
|
public static final String TAG = "PositionModel";
|
||||||
|
// 位置唯一标识符(与任务的positionId绑定)
|
||||||
|
String positionId;
|
||||||
|
// 经度(范围:-180~180)
|
||||||
|
double longitude;
|
||||||
|
// 纬度(范围:-90~90)
|
||||||
|
double latitude;
|
||||||
|
// 位置备注(空值时显示“无备注”)
|
||||||
|
String memo;
|
||||||
|
// 定位点与指定点实时距离长度
|
||||||
|
double realPositionDistance;
|
||||||
|
// 是否启用实时距离计算
|
||||||
|
boolean isEnableRealPositionDistance;
|
||||||
|
// 是否显示简单视图(true=简单视图,false=编辑视图)
|
||||||
|
boolean isSimpleView = true;
|
||||||
|
|
||||||
|
// 带参构造(强制初始化位置ID和经纬度)
|
||||||
|
public PositionModel(String positionId, double longitude, double latitude, String memo, boolean isEnableRealPositionDistance) {
|
||||||
|
this.positionId = (positionId == null || positionId.trim().isEmpty()) ? genPositionId() : positionId;
|
||||||
|
this.longitude = Math.max(-180, Math.min(180, longitude)); // 经度范围限制
|
||||||
|
this.latitude = Math.max(-90, Math.min(90, latitude)); // 纬度范围限制
|
||||||
|
this.memo = (memo == null || memo.trim().isEmpty()) ? "无备注" : memo;
|
||||||
|
this.isEnableRealPositionDistance = isEnableRealPositionDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无参构造(默认值初始化,避免空指针)
|
||||||
|
public PositionModel() {
|
||||||
|
this.positionId = genPositionId();
|
||||||
|
this.longitude = 0.0;
|
||||||
|
this.latitude = 0.0;
|
||||||
|
this.memo = "无备注";
|
||||||
|
this.isEnableRealPositionDistance = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRealPositionDistance(double realPositionDistance) {
|
||||||
|
this.realPositionDistance = realPositionDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getRealPositionDistance() {
|
||||||
|
return realPositionDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- Getter/Setter(确保字段有效性) ----------------------
|
||||||
|
public void setPositionId(String positionId) {
|
||||||
|
this.positionId = (positionId == null || positionId.trim().isEmpty()) ? genPositionId() : positionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPositionId() {
|
||||||
|
return positionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsEnableRealPositionDistance(boolean isEnableRealPositionDistance) {
|
||||||
|
this.isEnableRealPositionDistance = isEnableRealPositionDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnableRealPositionDistance() {
|
||||||
|
return isEnableRealPositionDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsSimpleView(boolean isSimpleView) {
|
||||||
|
this.isSimpleView = isSimpleView;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSimpleView() {
|
||||||
|
return isSimpleView;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMemo(String memo) {
|
||||||
|
this.memo = (memo == null || memo.trim().isEmpty()) ? "无备注" : memo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMemo() {
|
||||||
|
return memo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLongitude(double longitude) {
|
||||||
|
this.longitude = Math.max(-180, Math.min(180, longitude)); // 限制经度范围
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getLongitude() {
|
||||||
|
return longitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLatitude(double latitude) {
|
||||||
|
this.latitude = Math.max(-90, Math.min(90, latitude)); // 限制纬度范围
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getLatitude() {
|
||||||
|
return latitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- 父类方法重写 ----------------------
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return PositionModel.class.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成唯一位置ID(与任务ID格式一致,确保关联匹配)
|
||||||
|
public static String genPositionId() {
|
||||||
|
UUID uniqueUuid = UUID.randomUUID();
|
||||||
|
return uniqueUuid.toString(); // 36位标准UUID(含横杠,确保与任务ID格式统一)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON序列化(保存位置数据)
|
||||||
|
@Override
|
||||||
|
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||||
|
super.writeThisToJsonWriter(jsonWriter);
|
||||||
|
jsonWriter.name("positionId").value(getPositionId());
|
||||||
|
jsonWriter.name("longitude").value(getLongitude());
|
||||||
|
jsonWriter.name("latitude").value(getLatitude());
|
||||||
|
jsonWriter.name("memo").value(getMemo());
|
||||||
|
jsonWriter.name("isEnableRealPositionDistance").value(isEnableRealPositionDistance());
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON反序列化(加载位置数据,校验字段)
|
||||||
|
@Override
|
||||||
|
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||||
|
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
if (name.equals("positionId")) {
|
||||||
|
setPositionId(jsonReader.nextString());
|
||||||
|
} else if (name.equals("longitude")) {
|
||||||
|
setLongitude(jsonReader.nextDouble());
|
||||||
|
} else if (name.equals("latitude")) {
|
||||||
|
setLatitude(jsonReader.nextDouble());
|
||||||
|
} else if (name.equals("memo")) {
|
||||||
|
setMemo(jsonReader.nextString());
|
||||||
|
} else if (name.equals("isEnableRealPositionDistance")) {
|
||||||
|
setIsEnableRealPositionDistance(jsonReader.nextBoolean());
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从JSON读取位置数据
|
||||||
|
@Override
|
||||||
|
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||||
|
jsonReader.beginObject();
|
||||||
|
while (jsonReader.hasNext()) {
|
||||||
|
String name = jsonReader.nextName();
|
||||||
|
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||||
|
jsonReader.skipValue(); // 跳过未知字段
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsonReader.endObject();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- 核心工具方法:计算两点距离(Haversine公式,确保精度) ----------------------
|
||||||
|
/**
|
||||||
|
* 计算两个位置之间的直线距离(地球表面最短距离)
|
||||||
|
* @param position1 第一个位置(非null)
|
||||||
|
* @param position2 第二个位置(非null)
|
||||||
|
* @param isKilometer 是否返回千米单位:true→千米,false→米
|
||||||
|
* @return 距离(保留1位小数,符合显示需求)
|
||||||
|
* @throws IllegalArgumentException 位置为null或经纬度无效时抛出
|
||||||
|
*/
|
||||||
|
public static double calculatePositionDistance(PositionModel position1, PositionModel position2, boolean isKilometer) {
|
||||||
|
// 1. 校验参数有效性(避免计算异常)
|
||||||
|
if (position1 == null || position2 == null) {
|
||||||
|
throw new IllegalArgumentException("位置对象不能为null");
|
||||||
|
}
|
||||||
|
double lon1 = position1.getLongitude();
|
||||||
|
double lat1 = position1.getLatitude();
|
||||||
|
double lon2 = position2.getLongitude();
|
||||||
|
double lat2 = position2.getLatitude();
|
||||||
|
// 经纬度范围二次校验(确保有效)
|
||||||
|
if (lat1 < -90 || lat1 > 90 || lat2 < -90 || lat2 > 90
|
||||||
|
|| lon1 < -180 || lon1 > 180 || lon2 < -180 || lon2 > 180) {
|
||||||
|
throw new IllegalArgumentException("经纬度值无效(纬度:-90~90,经度:-180~180)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Haversine公式计算(地球半径取6371km,行业标准)
|
||||||
|
final double EARTH_RADIUS_KM = 6371;
|
||||||
|
double radLat1 = Math.toRadians(lat1); // 角度转弧度
|
||||||
|
double radLat2 = Math.toRadians(lat2);
|
||||||
|
double radLon1 = Math.toRadians(lon1);
|
||||||
|
double radLon2 = Math.toRadians(lon2);
|
||||||
|
|
||||||
|
double deltaLat = radLat2 - radLat1; // 纬度差
|
||||||
|
double deltaLon = radLon2 - radLon1; // 经度差
|
||||||
|
|
||||||
|
// 核心公式
|
||||||
|
double a = Math.pow(Math.sin(deltaLat / 2), 2)
|
||||||
|
+ Math.cos(radLat1) * Math.cos(radLat2)
|
||||||
|
* Math.pow(Math.sin(deltaLon / 2), 2);
|
||||||
|
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
double distanceKm = EARTH_RADIUS_KM * c; // 距离(千米)
|
||||||
|
|
||||||
|
// 3. 单位转换+精度处理(保留1位小数,符合显示需求)
|
||||||
|
double distance;
|
||||||
|
if (isKilometer) {
|
||||||
|
distance = Math.round(distanceKm * 10.0) / 10.0; // 千米→1位小数
|
||||||
|
} else {
|
||||||
|
distance = Math.round(distanceKm * 1000 * 10.0) / 10.0; // 米→1位小数
|
||||||
|
}
|
||||||
|
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package cc.winboll.studio.positions.models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
|
* @Date 2025/09/30 02:48
|
||||||
|
* @Describe 位置任务数据模型
|
||||||
|
*/
|
||||||
|
import android.util.JsonReader;
|
||||||
|
import android.util.JsonWriter;
|
||||||
|
import cc.winboll.studio.libappbase.BaseBean;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class PositionTaskModel extends BaseBean {
|
||||||
|
|
||||||
|
public static final String TAG = "PositionTaskModel";
|
||||||
|
// 任务标识符(唯一)
|
||||||
|
String taskId;
|
||||||
|
// 绑定的位置标识符(与PositionModel的positionId一一对应)
|
||||||
|
String positionId;
|
||||||
|
// 任务描述
|
||||||
|
String taskDescription;
|
||||||
|
// 任务距离条件:是否大于设定距离
|
||||||
|
boolean isGreaterThan;
|
||||||
|
// 任务距离条件:是否小于设定距离(与isGreaterThan互斥)
|
||||||
|
boolean isLessThan;
|
||||||
|
// 任务条件距离(单位:米)
|
||||||
|
int discussDistance;
|
||||||
|
// 任务开始启用时间
|
||||||
|
long startTime;
|
||||||
|
// 任务是否已触发
|
||||||
|
boolean isBingo = false;
|
||||||
|
// 是否启用任务
|
||||||
|
boolean isEnable;
|
||||||
|
|
||||||
|
// 带参构造(强制传入positionId,确保任务与位置绑定)
|
||||||
|
public PositionTaskModel(String taskId, String positionId, String taskDescription, boolean isGreaterThan, int discussDistance, long startTime, boolean isEnable) {
|
||||||
|
this.taskId = (taskId == null || taskId.trim().isEmpty()) ? genTaskId() : taskId; // 空ID自动生成
|
||||||
|
this.positionId = positionId; // 强制绑定位置ID
|
||||||
|
this.taskDescription = (taskDescription == null || taskDescription.trim().isEmpty()) ? "新任务" : taskDescription;
|
||||||
|
this.isGreaterThan = isGreaterThan;
|
||||||
|
this.isLessThan = !isGreaterThan; // 确保互斥
|
||||||
|
this.discussDistance = Math.max(discussDistance, 1); // 距离最小1米,避免无效值
|
||||||
|
this.startTime = startTime;
|
||||||
|
this.isEnable = isEnable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无参构造(初始化默认值,positionId需后续设置)
|
||||||
|
public PositionTaskModel() {
|
||||||
|
this.taskId = genTaskId();
|
||||||
|
this.positionId = "";
|
||||||
|
this.taskDescription = "新任务";
|
||||||
|
this.isGreaterThan = true;
|
||||||
|
this.isLessThan = false; // 初始互斥
|
||||||
|
this.discussDistance = 100; // 默认100米
|
||||||
|
this.startTime = System.currentTimeMillis();
|
||||||
|
this.isEnable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStartTime(long startTime) {
|
||||||
|
this.startTime = startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getStartTime() {
|
||||||
|
return startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsBingo(boolean isBingo) {
|
||||||
|
this.isBingo = isBingo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBingo() {
|
||||||
|
return isBingo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- Getter/Setter(确保positionId不为空,距离有效) ----------------------
|
||||||
|
public void setTaskId(String taskId) {
|
||||||
|
this.taskId = (taskId == null || taskId.trim().isEmpty()) ? genTaskId() : taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTaskId() {
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPositionId(String positionId) {
|
||||||
|
this.positionId = (positionId == null || positionId.trim().isEmpty()) ? "" : positionId; // 空值防护
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPositionId() {
|
||||||
|
return positionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTaskDescription(String taskDescription) {
|
||||||
|
this.taskDescription = (taskDescription == null || taskDescription.trim().isEmpty()) ? "新任务" : taskDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTaskDescription() {
|
||||||
|
return taskDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修复:确保isGreaterThan和isLessThan互斥
|
||||||
|
public void setIsGreaterThan(boolean isGreaterThan) {
|
||||||
|
this.isGreaterThan = isGreaterThan;
|
||||||
|
this.isLessThan = !isGreaterThan; // 关键:小于 = 非大于
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGreaterThan() {
|
||||||
|
return isGreaterThan;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修复:确保isLessThan和isGreaterThan互斥
|
||||||
|
public void setIsLessThan(boolean isLessThan) {
|
||||||
|
this.isLessThan = isLessThan;
|
||||||
|
this.isGreaterThan = !isLessThan; // 关键:大于 = 非小于
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLessThan() {
|
||||||
|
return isLessThan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDiscussDistance(int discussDistance) {
|
||||||
|
this.discussDistance = Math.max(discussDistance, 1); // 距离最小1米,避免0或负数
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDiscussDistance() {
|
||||||
|
return discussDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsEnable(boolean isEnable) {
|
||||||
|
this.isEnable = isEnable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnable() {
|
||||||
|
return isEnable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- 父类方法重写 ----------------------
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return PositionTaskModel.class.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成唯一任务ID(与PositionModel保持一致格式)
|
||||||
|
public static String genTaskId() {
|
||||||
|
UUID uniqueUuid = UUID.randomUUID();
|
||||||
|
return uniqueUuid.toString(); // 36位标准UUID(含横杠,确保唯一)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON序列化(保存任务数据,包含所有字段)
|
||||||
|
@Override
|
||||||
|
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||||
|
super.writeThisToJsonWriter(jsonWriter);
|
||||||
|
jsonWriter.name("taskId").value(getTaskId());
|
||||||
|
jsonWriter.name("positionId").value(getPositionId());
|
||||||
|
jsonWriter.name("taskDescription").value(getTaskDescription());
|
||||||
|
jsonWriter.name("isGreaterThan").value(isGreaterThan());
|
||||||
|
jsonWriter.name("isLessThan").value(isLessThan());
|
||||||
|
jsonWriter.name("discussDistance").value(getDiscussDistance());
|
||||||
|
jsonWriter.name("startTime").value(getStartTime());
|
||||||
|
jsonWriter.name("isEnable").value(isEnable());
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON反序列化(加载任务数据,校验字段有效性)
|
||||||
|
@Override
|
||||||
|
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||||
|
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
if (name.equals("taskId")) {
|
||||||
|
setTaskId(jsonReader.nextString());
|
||||||
|
} else if (name.equals("positionId")) {
|
||||||
|
setPositionId(jsonReader.nextString());
|
||||||
|
} else if (name.equals("taskDescription")) {
|
||||||
|
setTaskDescription(jsonReader.nextString());
|
||||||
|
} else if (name.equals("isGreaterThan")) {
|
||||||
|
setIsGreaterThan(jsonReader.nextBoolean());
|
||||||
|
} else if (name.equals("isLessThan")) {
|
||||||
|
setIsLessThan(jsonReader.nextBoolean());
|
||||||
|
} else if (name.equals("discussDistance")) {
|
||||||
|
setDiscussDistance(jsonReader.nextInt());
|
||||||
|
} else if (name.equals("startTime")) {
|
||||||
|
setStartTime(jsonReader.nextLong());
|
||||||
|
} else if (name.equals("isEnable")) {
|
||||||
|
setIsEnable(jsonReader.nextBoolean());
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从JSON读取任务数据(确保反序列化完整)
|
||||||
|
@Override
|
||||||
|
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||||
|
jsonReader.beginObject();
|
||||||
|
while (jsonReader.hasNext()) {
|
||||||
|
String name = jsonReader.nextName();
|
||||||
|
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||||
|
jsonReader.skipValue(); // 跳过未知字段,避免崩溃
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsonReader.endObject();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package cc.winboll.studio.positions.services;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2024/07/19 14:30:57
|
||||||
|
* @Describe 应用主要服务组件类守护进程服务组件类
|
||||||
|
*/
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import cc.winboll.studio.positions.services.MainService;
|
||||||
|
import cc.winboll.studio.positions.utils.AppConfigsUtil;
|
||||||
|
import cc.winboll.studio.positions.utils.ServiceUtil;
|
||||||
|
|
||||||
|
public class AssistantService extends Service {
|
||||||
|
|
||||||
|
public final static String TAG = "AssistantService";
|
||||||
|
public static final String EXTRA_IS_SETTING_TO_ENABLE = "EXTRA_IS_SETTING_TO_ENABLE";
|
||||||
|
|
||||||
|
MyServiceConnection mMyServiceConnection;
|
||||||
|
volatile boolean mIsServiceRunning;
|
||||||
|
AppConfigsUtil mAppConfigsUtil;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
mAppConfigsUtil = AppConfigsUtil.getInstance(this);
|
||||||
|
if (mMyServiceConnection == null) {
|
||||||
|
mMyServiceConnection = new MyServiceConnection();
|
||||||
|
}
|
||||||
|
// 设置运行参数
|
||||||
|
mIsServiceRunning = false;
|
||||||
|
if (mAppConfigsUtil.isEnableMainService(true)) {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
if (mAppConfigsUtil.isEnableMainService(true)) {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
|
return mAppConfigsUtil.isEnableMainService(true) ? Service.START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
mIsServiceRunning = false;
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 运行服务内容
|
||||||
|
//
|
||||||
|
void run() {
|
||||||
|
if (mAppConfigsUtil.isEnableMainService(true)) {
|
||||||
|
if (mIsServiceRunning == false) {
|
||||||
|
// 设置运行状态
|
||||||
|
mIsServiceRunning = true;
|
||||||
|
// 唤醒和绑定主进程
|
||||||
|
wakeupAndBindMain();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 唤醒和绑定主进程
|
||||||
|
//
|
||||||
|
void wakeupAndBindMain() {
|
||||||
|
if (ServiceUtil.isServiceAlive(getApplicationContext(), MainService.class.getName()) == false) {
|
||||||
|
startForegroundService(new Intent(AssistantService.this, MainService.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
bindService(new Intent(AssistantService.this, MainService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 主进程与守护进程连接时需要用到此类
|
||||||
|
//
|
||||||
|
class MyServiceConnection implements ServiceConnection {
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
if (mAppConfigsUtil.isEnableMainService(true)) {
|
||||||
|
wakeupAndBindMain();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
//package cc.winboll.studio.positions.services;
|
||||||
|
//
|
||||||
|
///**
|
||||||
|
// * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
|
// * @Date 2025/09/30 19:53
|
||||||
|
// * @Describe 位置距离服务:管理数据+定时计算距离+适配Adapter(Java 7 兼容)+ GPS信号加载
|
||||||
|
// */
|
||||||
|
//import android.app.Service;
|
||||||
|
//import android.content.Context;
|
||||||
|
//import android.content.Intent;
|
||||||
|
//import android.content.pm.PackageManager;
|
||||||
|
//import android.location.Location;
|
||||||
|
//import android.location.LocationListener;
|
||||||
|
//import android.location.LocationManager;
|
||||||
|
//import android.os.Binder;
|
||||||
|
//import android.os.Build;
|
||||||
|
//import android.os.Bundle;
|
||||||
|
//import android.os.IBinder;
|
||||||
|
//import android.os.Looper;
|
||||||
|
//import android.widget.Toast;
|
||||||
|
//import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
//import cc.winboll.studio.positions.adapters.PositionAdapter;
|
||||||
|
//import cc.winboll.studio.positions.models.AppConfigsModel;
|
||||||
|
//import cc.winboll.studio.positions.models.PositionModel;
|
||||||
|
//import cc.winboll.studio.positions.models.PositionTaskModel;
|
||||||
|
//import cc.winboll.studio.positions.utils.NotificationUtil;
|
||||||
|
//import java.util.ArrayList;
|
||||||
|
//import java.util.HashSet;
|
||||||
|
//import java.util.Iterator;
|
||||||
|
//import java.util.Set;
|
||||||
|
//import java.util.concurrent.Executors;
|
||||||
|
//import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
//import java.util.concurrent.TimeUnit;
|
||||||
|
//
|
||||||
|
///**
|
||||||
|
// * 核心职责:
|
||||||
|
// * 1. 实现 PositionAdapter.DistanceServiceInterface 接口,解耦Adapter与服务
|
||||||
|
// * 2. 单例式管理位置/任务数据,提供安全增删改查接口
|
||||||
|
// * 3. 后台单线程定时计算可见位置距离,主线程回调更新UI
|
||||||
|
// * 4. 内置GPS信号加载(通过LocationManager实时获取位置,解决“等待GPS信号”问题)
|
||||||
|
// * 5. 服务启动时启动前台通知(保活后台GPS功能,符合系统规范)
|
||||||
|
// * 6. 严格Java 7语法:无Lambda/Stream,显式迭代器/匿名内部类
|
||||||
|
// */
|
||||||
|
//public class DistanceRefreshService extends Service {
|
||||||
|
// public static final String TAG = "DistanceRefreshService";
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// // 服务状态与配置
|
||||||
|
// private boolean isServiceRunning = false;
|
||||||
|
//
|
||||||
|
// private static final int REFRESH_INTERVAL = 3; // 距离刷新间隔(秒)
|
||||||
|
// // 前台通知相关:记录是否已启动前台服务(避免重复调用startForeground)
|
||||||
|
// private boolean isForegroundServiceStarted = false;
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// // 服务绑定与UI回调
|
||||||
|
// private final IBinder mBinder = new DistanceBinder();
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * 在主线程显示Toast(避免子线程无法显示Toast的问题)
|
||||||
|
// */
|
||||||
|
// private void showToastOnMainThread(final String message) {
|
||||||
|
// if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||||
|
// Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
||||||
|
// } else {
|
||||||
|
// new android.os.Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||||
|
// @Override
|
||||||
|
// public void run() {
|
||||||
|
// Toast.makeText(DistanceRefreshService.this, message, Toast.LENGTH_SHORT).show();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // ---------------------- Binder 内部类(供外部绑定服务) ----------------------
|
||||||
|
// public class DistanceBinder extends Binder {
|
||||||
|
// /**
|
||||||
|
// * 外部绑定后获取服务实例(安全暴露服务引用)
|
||||||
|
// */
|
||||||
|
// public DistanceRefreshService getService() {
|
||||||
|
// return DistanceRefreshService.this;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// @Override
|
||||||
|
// public void onCreate() {
|
||||||
|
// super.onCreate();
|
||||||
|
//
|
||||||
|
// // 初始化GPS管理器(提前获取系统服务,避免启动时延迟)
|
||||||
|
// mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
||||||
|
// LogUtils.d(TAG, "服务 onCreate:初始化完成,等待启动命令");
|
||||||
|
// run();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @Override
|
||||||
|
// public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
// run();
|
||||||
|
// AppConfigsModel bean = AppConfigsModel.loadBean(DistanceRefreshService.this, AppConfigsModel.class);
|
||||||
|
// boolean isEnableService = (bean == null) ? false : bean.isEnableMainService();
|
||||||
|
// // 服务启用时返回START_STICKY(被杀死后尝试重启),禁用时返回默认值
|
||||||
|
// return isEnableService ? Service.START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public void run() {
|
||||||
|
// // 仅服务未运行时启动(避免重复启动)
|
||||||
|
// if (!isServiceRunning) {
|
||||||
|
// isServiceRunning = true;
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// startDistanceRefreshTask(); // 启动定时距离计算
|
||||||
|
// startForegroundNotification(); // 启动前台通知
|
||||||
|
//
|
||||||
|
// LogUtils.d(TAG, "服务 onStartCommand:启动成功,刷新间隔=" + REFRESH_INTERVAL + "秒,前台通知+GPS已启动");
|
||||||
|
// } else {
|
||||||
|
// LogUtils.w(TAG, "服务 onStartCommand:已在运行,无需重复启动(前台通知:" + (isForegroundServiceStarted ? "已启动" : "未启动") + " | GPS:" + (isGpsEnabled ? "已开启" : "未开启") + ")");
|
||||||
|
// // 异常场景恢复:补全未启动的组件
|
||||||
|
// if (!isForegroundServiceStarted) {
|
||||||
|
// startForegroundNotification();
|
||||||
|
// LogUtils.d(TAG, "服务 run:前台通知未启动,已恢复");
|
||||||
|
// }
|
||||||
|
// if (isServiceRunning && !isGpsEnabled) {
|
||||||
|
// startGpsLocation();
|
||||||
|
// LogUtils.d(TAG, "服务 run:GPS未启动,已恢复");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @Override
|
||||||
|
// public IBinder onBind(Intent intent) {
|
||||||
|
// return null; // 按你的业务逻辑返回,无绑定需求则保留null
|
||||||
|
// //LogUtils.d(TAG, "服务 onBind:外部绑定成功(运行状态:" + (isServiceRunning ? "是" : "否") + " | GPS状态:" + (isGpsEnabled ? "可用" : "不可用") + ")");
|
||||||
|
// //return mBinder; // 返回Binder实例,供外部获取服务
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /*@Override
|
||||||
|
// public boolean onUnbind(Intent intent) {
|
||||||
|
// LogUtils.d(TAG, "服务 onUnbind:外部解绑,清理回调与可见位置");
|
||||||
|
// // 解绑后清理资源,避免内存泄漏
|
||||||
|
// mDistanceReceiver = null;
|
||||||
|
// mVisiblePositionIds.clear();
|
||||||
|
// // 解绑时不停止GPS(服务仍在后台运行,需持续获取位置)
|
||||||
|
// return super.onUnbind(intent);
|
||||||
|
// }*/
|
||||||
|
//
|
||||||
|
// @Override
|
||||||
|
// public void onDestroy() {
|
||||||
|
// super.onDestroy();
|
||||||
|
//
|
||||||
|
// LogUtils.d(TAG, "服务 onDestroy:销毁完成,资源已释放(GPS+前台通知+线程池)");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // ---------------------- 前台服务通知管理(与GPS状态联动优化) ----------------------
|
||||||
|
// /**
|
||||||
|
// * 启动前台服务通知(调用NotificationUtils创建通知,确保仅启动一次)
|
||||||
|
// */
|
||||||
|
// private void startForegroundNotification() {
|
||||||
|
// // 1. 校验:避免重复调用startForeground(系统不允许重复启动)
|
||||||
|
// if (isForegroundServiceStarted) {
|
||||||
|
// LogUtils.w(TAG, "startForegroundNotification:前台通知已启动,无需重复执行");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// try {
|
||||||
|
//// 2. 初始化通知状态文本(根据GPS初始状态动态显示,避免固定“等待”)
|
||||||
|
// String initialStatus;
|
||||||
|
// if (isGpsPermissionGranted && isGpsEnabled) {
|
||||||
|
// initialStatus = "GPS已就绪,正在获取位置(刷新间隔" + REFRESH_INTERVAL + "秒)";
|
||||||
|
// } else if (!isGpsPermissionGranted) {
|
||||||
|
// initialStatus = "缺少定位权限,无法获取GPS位置";
|
||||||
|
// } else {
|
||||||
|
// initialStatus = "GPS未开启,请在设置中打开";
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//// 5. 标记前台服务已启动
|
||||||
|
// isForegroundServiceStarted = true;
|
||||||
|
// LogUtils.d(TAG, "startForegroundNotification:前台服务通知启动成功,初始状态:" + initialStatus);
|
||||||
|
//
|
||||||
|
// } catch (Exception e) {
|
||||||
|
//// 捕获异常(如上下文失效、通知渠道未创建)
|
||||||
|
// isForegroundServiceStarted = false;
|
||||||
|
// LogUtils.d(TAG, "startForegroundNotification:前台通知启动失败" + e);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
//
|
||||||
|
// - 主线程回调Adapter更新UI(避免跨线程操作UI异常)
|
||||||
|
// */
|
||||||
|
// /*private void notifyDistanceUpdateToUI(final String positionId) {
|
||||||
|
// if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||||
|
// if (mDistanceReceiver != null) {
|
||||||
|
// mDistanceReceiver.onDistanceUpdate(positionId);
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// new android.os.Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||||
|
// @Override
|
||||||
|
// public void run() {
|
||||||
|
// if (mDistanceReceiver != null) {
|
||||||
|
// mDistanceReceiver.onDistanceUpdate(positionId);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }*/
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//// ---------------------- 实现 PositionAdapter.DistanceServiceInterface 接口 ----------------------
|
||||||
|
//
|
||||||
|
// public ArrayList getPositionList() {
|
||||||
|
// if (!isServiceRunning) {
|
||||||
|
// LogUtils.w(TAG, "getPositionList:服务未运行,返回空列表");
|
||||||
|
// return new ArrayList();
|
||||||
|
// }
|
||||||
|
// return new ArrayList(mPositionList);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// public ArrayList getPositionTasksList() {
|
||||||
|
// if (!isServiceRunning) {
|
||||||
|
// LogUtils.w(TAG, "getPositionTasksList:服务未运行,返回空列表");
|
||||||
|
// return new ArrayList();
|
||||||
|
// }
|
||||||
|
// return new ArrayList(mTaskList);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// /*public void setOnDistanceUpdateReceiver(PositionAdapter.OnDistanceUpdateReceiver receiver) {
|
||||||
|
// this.mDistanceReceiver = receiver;
|
||||||
|
// LogUtils.d(TAG, "setOnDistanceUpdateReceiver:回调接收器已设置(" + (receiver != null ? "有效" : "无效") + ")");
|
||||||
|
// }*/
|
||||||
|
//
|
||||||
|
// public void addVisibleDistanceView(String positionId) {
|
||||||
|
// if (!isServiceRunning || positionId == null) {
|
||||||
|
// LogUtils.w(TAG, "addVisibleDistanceView:服务未运行/位置ID无效,添加失败");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (mVisiblePositionIds.add(positionId)) {
|
||||||
|
// LogUtils.d(TAG, "addVisibleDistanceView:添加成功(位置ID=" + positionId + ",当前可见数=" + mVisiblePositionIds.size() + ")");
|
||||||
|
//// 新增:添加可见位置后,立即更新通知(显示最新可见数量)
|
||||||
|
// if (isForegroundServiceStarted && mCurrentGpsPosition != null) {
|
||||||
|
// syncGpsStatusToNotification();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public void removeVisibleDistanceView(String positionId) {
|
||||||
|
// if (positionId == null) {
|
||||||
|
// LogUtils.w(TAG, "removeVisibleDistanceView:位置ID为空,移除失败");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (mVisiblePositionIds.remove(positionId)) {
|
||||||
|
// int remainingCount = mVisiblePositionIds.size();
|
||||||
|
// LogUtils.d(TAG, "removeVisibleDistanceView:移除成功(位置ID=" + positionId + ",当前可见数=" + remainingCount + ")");
|
||||||
|
//// 新增:移除可见位置后,更新通知(同步数量变化)
|
||||||
|
// if (isForegroundServiceStarted && mCurrentGpsPosition != null) {
|
||||||
|
// syncGpsStatusToNotification();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public void clearVisibleDistanceViews() {
|
||||||
|
// mVisiblePositionIds.clear();
|
||||||
|
// LogUtils.d(TAG, "clearVisibleDistanceViews:所有可见位置已清空");
|
||||||
|
//// 新增:清空可见位置后,更新通知(提示计算暂停)
|
||||||
|
// if (isForegroundServiceStarted) {
|
||||||
|
// updateNotificationGpsStatus("无可见位置,距离计算暂停");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//// ---------------------- 数据管理接口(修复原有语法错误+优化逻辑) ----------------------
|
||||||
|
// /**
|
||||||
|
//
|
||||||
|
// - 获取服务运行状态
|
||||||
|
// */
|
||||||
|
// public boolean isServiceRunning() {
|
||||||
|
// return isServiceRunning;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
//
|
||||||
|
// - 添加位置(修复迭代器泛型缺失问题)
|
||||||
|
// */
|
||||||
|
// public void addPosition(PositionModel position) {
|
||||||
|
// if (!isServiceRunning || position == null || position.getPositionId() == null) {
|
||||||
|
// LogUtils.w(TAG, "addPosition:服务未运行/数据无效,添加失败");
|
||||||
|
// return;
|
||||||
|
// }// 修复:显式声明PositionModel泛型,避免类型转换警告
|
||||||
|
// boolean isDuplicate = false;
|
||||||
|
// Iterator posIter = mPositionList.iterator();
|
||||||
|
// while (posIter.hasNext()) {
|
||||||
|
// PositionModel existingPos = (PositionModel)posIter.next();
|
||||||
|
// if (position.getPositionId().equals(existingPos.getPositionId())) {
|
||||||
|
// isDuplicate = true;
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }if (!isDuplicate) {
|
||||||
|
// mPositionList.add(position);
|
||||||
|
// LogUtils.d(TAG, "addPosition:添加成功(位置ID=" + position.getPositionId() + ",总数=" + mPositionList.size() + ")");
|
||||||
|
// } else {
|
||||||
|
// LogUtils.w(TAG, "addPosition:位置ID=" + position.getPositionId() + "已存在,添加失败");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
//
|
||||||
|
// - 删除位置(修复任务删除时的类型转换错误)
|
||||||
|
// */
|
||||||
|
// public void removePosition(String positionId) {
|
||||||
|
// if (!isServiceRunning || positionId == null) {
|
||||||
|
// LogUtils.w(TAG, "removePosition:服务未运行/位置ID无效,删除失败");
|
||||||
|
// return;
|
||||||
|
// }// 1. 删除位置
|
||||||
|
// boolean isRemoved = false;
|
||||||
|
// Iterator posIter = mPositionList.iterator();
|
||||||
|
// while (posIter.hasNext()) {
|
||||||
|
// PositionModel pos = (PositionModel)posIter.next();
|
||||||
|
// if (positionId.equals(pos.getPositionId())) {
|
||||||
|
// posIter.remove();
|
||||||
|
// isRemoved = true;
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }if (isRemoved) {
|
||||||
|
//// 修复:任务列表迭代时用PositionTaskModel泛型(原错误用PositionModel导致转换失败)
|
||||||
|
// Iterator taskIter = mTaskList.iterator();
|
||||||
|
// while (taskIter.hasNext()) {
|
||||||
|
// PositionTaskModel task = (PositionTaskModel)taskIter.next();
|
||||||
|
// if (positionId.equals(task.getPositionId())) {
|
||||||
|
// taskIter.remove();
|
||||||
|
// }
|
||||||
|
// }// 3. 移除可见位置
|
||||||
|
// mVisiblePositionIds.remove(positionId);
|
||||||
|
// LogUtils.d(TAG, "removePosition:删除成功(位置ID=" + positionId + ",剩余位置数=" + mPositionList.size() + ",剩余任务数=" + mTaskList.size() + ")");
|
||||||
|
// } else {
|
||||||
|
// LogUtils.w(TAG, "removePosition:位置ID=" + positionId + "不存在,删除失败");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
//
|
||||||
|
// - 更新位置信息(修复代码格式+迭代器泛型)
|
||||||
|
// */
|
||||||
|
// public void updatePosition(PositionModel updatedPosition) {
|
||||||
|
// if (!isServiceRunning || updatedPosition == null || updatedPosition.getPositionId() == null) {
|
||||||
|
// LogUtils.w(TAG, "updatePosition:服务未运行/数据无效,更新失败");
|
||||||
|
// return;
|
||||||
|
// }boolean isUpdated = false;
|
||||||
|
// Iterator posIter = mPositionList.iterator();
|
||||||
|
// while (posIter.hasNext()) {
|
||||||
|
// PositionModel pos = (PositionModel)posIter.next();
|
||||||
|
// if (updatedPosition.getPositionId().equals(pos.getPositionId())) {
|
||||||
|
// pos.setMemo(updatedPosition.getMemo());
|
||||||
|
// pos.setIsEnableRealPositionDistance(updatedPosition.isEnableRealPositionDistance());
|
||||||
|
// if (!updatedPosition.isEnableRealPositionDistance()) {
|
||||||
|
// pos.setRealPositionDistance(-1);
|
||||||
|
// //notifyDistanceUpdateToUI(pos.getPositionId());
|
||||||
|
// }
|
||||||
|
// isUpdated = true;
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }if (isUpdated) {
|
||||||
|
// LogUtils.d(TAG, "updatePosition:更新成功(位置ID=" + updatedPosition.getPositionId() + ")");
|
||||||
|
// } else {
|
||||||
|
// LogUtils.w(TAG, "updatePosition:位置ID=" + updatedPosition.getPositionId() + "不存在,更新失败");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
//
|
||||||
|
// - 同步任务列表(修复泛型缺失+代码格式)
|
||||||
|
// */
|
||||||
|
// public void syncAllPositionTasks(ArrayList tasks) {
|
||||||
|
// if (!isServiceRunning || tasks == null) {
|
||||||
|
// LogUtils.w(TAG, "syncAllPositionTasks:服务未运行/任务列表为空,同步失败");
|
||||||
|
// return;
|
||||||
|
// }// 1. 清空旧任务
|
||||||
|
// mTaskList.clear();
|
||||||
|
//// 2. 添加新任务(修复泛型+去重逻辑)
|
||||||
|
// Set taskIdSet = new HashSet();
|
||||||
|
// Iterator taskIter = tasks.iterator();
|
||||||
|
// while (taskIter.hasNext()) {
|
||||||
|
// PositionTaskModel task = (PositionTaskModel)taskIter.next();
|
||||||
|
// if (task != null && task.getTaskId() != null && !taskIdSet.contains(task.getTaskId())) {
|
||||||
|
// taskIdSet.add(task.getTaskId());
|
||||||
|
// mTaskList.add(task);
|
||||||
|
// }
|
||||||
|
// }LogUtils.d(TAG, "syncAllPositionTasks:同步成功(接收任务数=" + tasks.size() + ",去重后=" + mTaskList.size() + ")");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//// ---------------------- 补充:修复LocationProvider引用缺失问题(避免编译错误) ----------------------
|
||||||
|
//// 注:原代码中onStatusChanged使用LocationProvider枚举,需补充静态导入或显式声明
|
||||||
|
//// 此处通过内部静态类定义,解决系统API引用问题(兼容Java 7语法)
|
||||||
|
// private static class LocationProvider {
|
||||||
|
// public static final int AVAILABLE = 2;
|
||||||
|
// public static final int OUT_OF_SERVICE = 0;
|
||||||
|
// public static final int TEMPORARILY_UNAVAILABLE = 1;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//// ---------------------- 补充:Context引用工具(避免服务销毁后Context失效) ----------------------
|
||||||
|
// /*private Context getSafeContext() {
|
||||||
|
// // 服务未销毁时返回自身Context,已销毁时返回应用Context(避免内存泄漏)
|
||||||
|
// if (isDestroyed()) {
|
||||||
|
// return getApplicationContext();
|
||||||
|
// }
|
||||||
|
// return this;
|
||||||
|
// }*/
|
||||||
|
//
|
||||||
|
//// 注:isDestroyed()为API 17+方法,若需兼容更低版本,可添加版本判断
|
||||||
|
// /*private boolean isDestroyed() {
|
||||||
|
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
|
// return super.isDestroyed();
|
||||||
|
// }
|
||||||
|
// // 低版本通过状态标记间接判断(服务销毁时会置为false)
|
||||||
|
// return !isServiceRunning && !isForegroundServiceStarted;
|
||||||
|
// }*/
|
||||||
|
//}
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user