SD - Slam Dump(并不是)
这个App的主要目的是满足广大人民群众对图片编辑的需求。
字体问题
Android默认的字体不太好看,也不一定能很好地匹配背景图。如果内置字体,遇到最大的问题是版权问题。 因此决定增加用户自行导入字体的功能,由用户来决定使用什么字体。
原来的字体文件是放在asset中。Typeface.createFromAsset
直接引入并使用。
Typeface tf = Typeface.createFromAsset(mgr, "fonts/fz_grid.ttf");
mContentTv.setTypeface(tf);
设计一个字体管理界面。用户自行选择将字体文件复制到App内部存储路径。
使用字体时,再用Typeface.createFromFile()
获取Typeface。
选择文件
调用系统文件选择器
代码语言:javascript复制private static final int REQ_CODE_CHOOSE_FILE = 10;
// 启动选择文件...
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
startActivityForResult(intent, REQ_CODE_CHOOSE_FILE);
// ......
// 处理选择的文件
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
switch (requestCode) {
case REQ_CODE_CHOOSE_FILE:
if (data != null) {
Uri uri = data.getData();
Log.d(TAG, "onActivityResult: uri: " uri);
if (uri != null && !TextUtils.isEmpty(uri.getPath())) {
copyFile(uri);
} else {
Log.e(TAG, "onActivityResult: 选择的文件无效");
}
} else {
showShort(getApplicationContext(), "没选中文件");
Log.e(TAG, "onActivityResult: data is NULL 没选中文件");
}
break;
default:
super.onActivityResult(requestCode, resultCode, data);
break;
}
}
处理uri
uri形如
content://com.android.externalstorage.documents/document/primary:Download/fz_grid.ttf
uri.getPath获取到的并不是文件的绝对路径。但我们可以利用ContentResolver来获取到InputStream。 也可以获取到uri的文件名。
代码语言:javascript复制private void copyFile(final Uri uri) {
mAddIv.setClickable(false);
Animation rotate = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.rotate_scan);
rotate.setDuration(400);
mAddIv.startAnimation(rotate);
new Thread(new Runnable() {
@Override
public void run() {
try {
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
cursor.moveToFirst();
String name = cursor.getString(nameIndex);
cursor.close();
InputStream fis = getContentResolver().openInputStream(uri);
File outputFile = new File(TypefaceStore.getStorePath(getApplicationContext()), name);
if (outputFile.exists()) {
boolean d = outputFile.delete();
Log.d(TAG, "删除旧文件: " d);
}
boolean n = outputFile.createNewFile();
Log.d(TAG, "copyFile: 新建文件 " n);
FileOutputStream fos = new FileOutputStream(outputFile);
byte[] tmp = new byte[2048];
int i;
while ((i = fis.read(tmp)) != -1) {
fos.write(tmp, 0, i);
}
fos.flush();
fos.close();
fis.close();
} catch (Exception e) {
Log.e(TAG, "copyFile ERROR:", e);
}
}
}).start();
}
也可以简单地使用uri.getLastPathSegment来获取文件名
代码语言:javascript复制uri.getLastPathSegment();
String[] t = uriPath.split(File.separator);
String name = t[t.length - 1];
https://stackoverflow.com/questions/4263002/how-to-get-file-name-from-uri
Toolbar问题
使用toolbar时经常会遇到问题。例如设置title的问题。
这里自己创建一个统一的标题栏TitleBar。想要什么控件自己添加。
Google MobileAds
MobileAds.initialize(getApplicationContext(), AdsMgr.GOOGLE_ADS_APP_ID);
的执行会占用很多时间。测试过程中发现小米手机甚至使用了3秒钟来执行这个方法。
https://stackoverflow.com/questions/37418663/what-is-the-proper-way-to-call-mobileads-initialize
给启动页Activity一个纯色的启动背景。
代码语言:javascript复制<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowBackground">@color/colorPrimary</item>
</style>
启动页中初始化Ads时实在是耗时太长,干脆放到子线程中去操作。
虽然官方文档建议的是越早初始化越好。但也不希望太影响用户体验。
递归查看某个路径下的文件
代码语言:javascript复制 private static void treeDir(File dir, int level) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < level; i ) {
sb.append("-");
}
sb.append(" ");
if (dir.isDirectory()) {
// LL.d(TAG, sb.toString() dir.getName());
level ;
for (File f : dir.listFiles()) {
treeDir(f, level);
}
} else {
// LL.d(TAG, sb.toString() dir.getName());
}
}
提供草稿功能
为方便用户使用,提供草稿功能。这就涉及到增删查改的操作。
[2019-7-31] 本来想直接用sqlite,但为了开发方便,选用了greenDAO。
https://github.com/greenrobot/greenDAO
使用2个表,分别为Draft(存档)和DraftContent(图层)。DraftContent中存放着关联的存档ID。
能保存的东西都保存下来。
greendao插入元素
代码语言:javascript复制Draft draft1 = genDraft("示例1", p1Path);
Draft draft2 = genDraft("示例2", p2Path);
Draft draft3 = genDraft("示例3", p3Path);
Log.d(TAG, "addDemoDraft: id: " draft1.getDraftId() "," draft3.getDraftId());
daoSession.insert(draft1);
daoSession.insert(draft2);
daoSession.insert(draft3);
插入元素后就有id了。
greendao删除元素
代码语言:javascript复制DraftDao draftDao = daoSession.getDraftDao();
DraftContentDao draftContentDao = daoSession.getDraftContentDao();
for (Draft d : drafts) {
Log.d(TAG, "删除 " d.getName());
draftDao.queryBuilder()
.where(DraftDao.Properties.DraftId.eq(d.getDraftId())).buildDelete()
.executeDeleteWithoutDetachingEntities();
draftContentDao.queryBuilder()
.where(DraftContentDao.Properties.RelativeDraftId.eq(d.getDraftId())).buildDelete()
.executeDeleteWithoutDetachingEntities();
}
使用DrawerLayout
报错: IllegalArgumentException: No drawer view found with gravity LEFT
java.lang.IllegalArgumentException: No drawer view found with gravity LEFT
at androidx.drawerlayout.widget.DrawerLayout.openDrawer(DrawerLayout.java:1736)
at androidx.drawerlayout.widget.DrawerLayout.openDrawer(DrawerLayout.java:1722)
忘记中xml中加上开抽屉方向了 tools:openDrawer=”start”
代码语言:javascript复制<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@ id/main_page_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".act.MainActivity"
tools:openDrawer="start">
抽屉加上方向 android:layout_gravity=”start”
代码语言:javascript复制<!-- 抽屉 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
android:layout_marginEnd="100dp"
android:orientation="vertical">
美术设计,App交互设计
设计是一个比较令我头疼的问题。在这个看脸的时代,App一定要好看!对我而言,直接采用material design的风格会比较省事。 经过调整和对比,我选择使用暗色的风格。因为现在主流的图形编辑软件,颜色风格以暗色居多。
参考:
- 看颜色示例 https://material.io/design/color/applying-color-to-ui.html#sheets-surfaces
- 查颜色 https://material-ui.com/customization/color/
文字编辑
文字内容,大小,旋转方向,颜色都可以调整。
需要一个调色盘来调整颜色。找个第三方的,好看能用即可。
删除存档报错
list类的经典异常 ConcurrentModificationException。
代码语言:javascript复制java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.next(ArrayList.java:860)
list删除元素时报错。这样写是不行的。
代码语言:javascript复制for (Data d : dataList) {
if (d.selected) {
dataList.remove(d);
}
}
用迭代器来删除元素。
代码语言:javascript复制Iterator<Data> iterator = dataList.iterator();
while (iterator.hasNext()) {
Data data = iterator.next();
if (data.selected) {
iterator.remove();
}
}
输出图片
保存View的显示内容
获取一个view的bitmap,然后保存到文件去。
代码语言:javascript复制/**
* 获取一个 View 的缓存视图
*/
private Bitmap getCacheBitmapFromView(View view) {
final boolean drawingCacheEnabled = true;
view.setDrawingCacheEnabled(drawingCacheEnabled);
view.buildDrawingCache(drawingCacheEnabled);
final Bitmap drawingCache = view.getDrawingCache();
Bitmap bitmap;
if (drawingCache != null) {
bitmap = Bitmap.createBitmap(drawingCache);
view.setDrawingCacheEnabled(false);
} else {
bitmap = null;
}
return bitmap;
}
public static boolean saveBitmapFile(Bitmap bitmap, String fileAbsPath) {
File file = new File(fileAbsPath); // 将要保存图片的路径
try {
if (file.exists()) {
file.delete();
}
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
bos.flush();
bos.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
保存图片文件后的处理
用户输出图片文件后,打开微信想发送这张图片。但是用户发现微信的快捷发送功能找不到这张图片。 怎么才能让微信知道这里新增了一张图片呢?
如果要发送广播ACTION_MEDIA_MOUNTED
sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.fromFile(outputFile)));
报错,没有足够的权限
代码语言:javascript复制java.lang.SecurityException: Permission Denial: not allowed to send broadcast android.intent.action.MEDIA_MOUNTED
Android KK开始,这个广播开始只能由系统发出。KK及之后的版本需使用Intent.ACTION_MEDIA_SCANNER_SCAN_FILE
File outputFile = new File(filePath);
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(outputFile)));
参考
https://stackoverflow.com/questions/24072489/java-lang-securityexception-permission-denial-not-allowed-to-send-broadcast-an
移动TextView
编辑页中有一个需求是手指拖动文字。
1.1.x版本
1.1.0版本的做法是,在Activity的onTouch方法里来改变TextView的坐标。从而实现TextView的拖动效果。 父View和子View设同一个OnTouchListener。但是只有父view来处理触摸事件。 如果是子view接收到了触摸事件,则做一个bool标记firstOnTv = true,返回false,把触摸事件交给父view来处理。 父view处理触摸事件时,判断如果刚才点中的是子view(即mContentTv),则在MotionEvent.ACTION_MOVE时更改子view的坐标。
代码语言:javascript复制 private View.OnTouchListener mWsOnTouchListener = new View.OnTouchListener() {
boolean firstOnTv = false; // 最开始点中的是tv
float originTvX; // tv最开始的坐标
float originTvY;
float downX;
float downY;
@Override
public boolean onTouch(View v, MotionEvent event) {
final int id = v.getId();
// Log.d(TAG, "onTouch: touch tv: " (id == mContentTv.getId()) ", touch ws: " (id == mWorkspaceField.getId()));
float x = event.getX();
float y = event.getY();
// Log.d(TAG, "onTouch: [" x ", " y "] , " event);
if (event.getAction() == MotionEvent.ACTION_DOWN) {
mSaveIv.setEnabled(true);
if (id == mContentTv.getId()) {
firstOnTv = true;
originTvX = mContentTv.getX();
originTvY = mContentTv.getY();
// Log.d(TAG, "onTouch: 保存tv坐标 (" originTvX ", " originTvY ")");
}
downX = event.getX();
downY = event.getY();
// Log.d(TAG, "onTouch: down: x,y [" x ", " y "]");
return id != mContentTv.getId();
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
// Log.d(TAG, "onTouch: move: x,y [" x ", " y "]");
if (!firstOnTv) {
return true; // 不移动tv,直接消耗掉这个操作
}
float dx = x - downX;
float dy = y - downY;
if (Math.abs(dx) > 2 && Math.abs(dy) > 2) {
mContentTv.setX(originTvX dx);
mContentTv.setY(originTvY dy);
}
return true;
} else if (event.getAction() == MotionEvent.ACTION_UP) {
firstOnTv = false;
if (mCanvasWid > 0 && mCanvasHeight > 0) {
mDraftContent.setTvLocationXRatio(mContentTv.getX() / mCanvasWid);
mDraftContent.setTvLocationYRatio(mContentTv.getY() / mCanvasHeight);
}
return true;
}
return false;
}
};
版本更新
- 2019-8-8 v1.1.1 版本更新
- 解决了一些bug
- UI调整,增加了抽屉的头图和欢迎文字
- 2019-8-4 v1.1.0 版本更新