「SD编辑」开发记录

2021-02-01 15:17:36 浏览数 (1)

SD - Slam Dump(并不是)

这个App的主要目的是满足广大人民群众对图片编辑的需求。

字体问题

Android默认的字体不太好看,也不一定能很好地匹配背景图。如果内置字体,遇到最大的问题是版权问题。 因此决定增加用户自行导入字体的功能,由用户来决定使用什么字体。

原来的字体文件是放在asset中。Typeface.createFromAsset直接引入并使用。

代码语言:javascript复制
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

代码语言:javascript复制
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

代码语言:javascript复制
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

代码语言:javascript复制
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 版本更新

0 人点赞