暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

鸿蒙开源第三方件:图片裁剪组件

鸿蒙技术社区 2021-04-06
608

基于安卓平台的图片裁剪组件 uCrop( https://github.com/Yalantis/uCrop),实现了鸿蒙化迁移和重构。目前代码已经开源,欢迎各位下载使用并提出宝贵意见!


开源代码:

https://gitee.com/isrc_ohos/u-crop_ohos


uCrop 组件是开源的图片裁剪库,支持对图片的缩放和裁剪等操作,是安卓平台比较受欢迎的组件,在 Github 上已有 1 万多个 Star 和近 2 千个 Fork。


uCrop 组件具有封装程度高、使用流畅、自定义程度高的优点,被广泛应用于多种 APP 中。


01

组件效果展示


安卓和鸿蒙 UI 组件的差异较大,uCrop_ohos 的实现完全重构了安卓版 uCrop 的 UI 部分,所以 uCrop_ohos 的组件效果看上去会和 uCrop 完全不同。


本组件的效果展示可分为两个步骤:图片选择和图片裁剪。下面依次对其进行讲解和展示。


①uCrop_ohos 图片选择


uCrop_ohos 支持裁剪系统选择相册图片或网络图片,用户可以在主菜单中选择对应的功能。


如图 1 所示:

图 1:主菜单界面


uCrop_ohos 读取相册图片:当用户赋予组件相应权限后,uCrop_ohos 可以自动读取手机相册中每一张图片,并将它们的缩略图作为一个列表呈现在 UI 界面上,用户可以上下滑动列表寻找目标图片。


如图 2 所示,当用户点击某张缩略图时,会跳转到 uCrop_ohos 的裁剪界面,执行后续操作。

图 2:选择系统相册图片


uCrop_ohos 读取网络图片:用户需要将图片网址键入到输入框内并点击确定按钮。


如图 3 所示,uCrop_ohos 会自动下载图片并跳转到裁剪界面,执行后续操作。

图 3:选择网络图片


②uCrop_ohos 图片裁剪


图 4:uCrop_ohos 的裁剪界面


图 4 是 uCrop_ohos 的裁剪界面。使用者可以通过手势对图片进行缩放、旋转和平移的操作,也可以通过按钮、滑块等控件进行相应操作。


将图片调整至满意状态时,点击裁剪按钮即可获得裁剪后的新图片,并将其保存至手机相册。


且本组件的图片与裁剪框具有自适应能力,能够保证裁剪框时刻在图片范围内,防止由于裁剪框的范围大于图片导致的一系列问题。


02

Sample 解析


图 5:Sample 的工程结构


uCrop_ohos 的核心能力都由其 Library 提供,Sample 主要用于构建 UI,并调用 Library 的接口。


从图 5 可以看出 Sample 的工程结构较为简单,主要由 4 个文件构成,下面进行详细的介绍。


①CropPicture


CropPicture 文件提供了裁剪界面,其最主要的逻辑是通过图片 Uri 实例化 Library 中 UCropView 类。


由于 uCrop_ohos 的逻辑是先将用户选择的原图创建一个副本,然后对副本执行裁剪。


所以为了将图片传入 UCropView 需要两个 Uri:

  • 一个名为 uri_i,从 intent 中获得,标识的是用户选择的原图,可以是本地图片也可以是网络图片。

  • 另一个名为 uri_o,标识的是原图副本,一定是一张本地图片。


代码如下:
//URI_IN
Uri uri_i = intent.getUri();

//URI_OUT
String filename = "test.jpg";
PixelMap.InitializationOptions options = new PixelMap.InitializationOptions();
options.size = new Size(100,100);
PixelMap pixelmap = PixelMap.create(options);
Uri uri_o = saveImage(filename, pixelmap);

//UcropView
UCropView uCropView = new UCropView(this);
try {
    uCropView.getCropImageView().setImageUri(uri_i, uri_o);
    uCropView.getOverlayView().setShowCropFrame(true);
    uCropView.getOverlayView().setShowCropGrid(true);
    uCropView.getOverlayView().setDimmedColor(Color.TRANSPARENT.getValue());

catch (Exception e) {
    e.printStackTrace();
}


Library 给开发者提供了 public 接口,使得开发者易于封装自己的 UI 功能。


例如本文件中的旋转和缩放滑块、旋转和缩放按钮、当前旋转和缩放状态的显示都是调用 Library 接口实现的。


以如下功能的实现为例:创建了一个按钮,当用户触碰这个按钮之后就可以将图片右旋 90 度。


其核心能力就是依靠调用 Library 中 postRotate() 函数实现的,非常简单。
//右旋90度的Button
Button button_plus_90 = new Button(this);
button_plus_90.setText("+90°");
button_plus_90.setTextSize(80);
button_plus_90.setBackground(buttonBackground);
button_plus_90.setClickedListener(new Component.ClickedListener() {
    @Override
    public void onClick(Component component) {
        float degrees = 90f;
        //计算旋转中心
        float center_X = uCropView.getOverlayView().getCropViewRect().getCenter().getPointX();
        float center_Y = uCropView.getOverlayView().getCropViewRect().getCenter().getPointY();
        //旋转
        uCropView.getCropImageView().postRotate(degrees,center_X,center_Y);
        //适配
        uCropView.getCropImageView().setImageToWrapCropBounds(false);
        //显示旋转角度
        mDegree = uCropView.getCropImageView().getCurrentAngle();
        text.setText("当前旋转角度: " + df.format(mDegree) + " °");
    }
});


②LocalPictureChoose & HttpPictureChoose


由上文可知,uri_i 是通过 intent 得到的,这个 intent 就是由 LocalPictureChoose 或 HttpPictureChoose 传递的。


LocalPictureChoose 提供选择相册图片的能力,HttpPictureChoose 提供选择网络图片的能力。


LocalPictureChoose 提供的功能是将相册中的全部图片读取出来,做成缩略图排列在 UI 上,然后将每个缩略图绑定一个触摸监听器。


一旦使用者选中某个缩略图,就会将这个缩略图对应的原图 uri 放在 intent 中传给 CropPicture。


具体代码如下:
private void showImage() {
    DataAbilityHelper helper = DataAbilityHelper.creator(this);
    try {
        // columns为null,查询记录所有字段,当前例子表示查询id字段
        ResultSet resultSet = helper.query(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI, new String[]{AVStorage.Images.Media.ID}, null);
        while (resultSet != null && resultSet.goToNextRow()) {
            //创建image用以显示系统相册缩略图
            PixelMap pixelMap = null;
            ImageSource imageSource = null;
            Image image = new Image(this);
            image.setWidth(250);
            image.setHeight(250);
            image.setMarginsLeftAndRight(1010);
            image.setMarginsTopAndBottom(1010);
            image.setScaleMode(Image.ScaleMode.CLIP_CENTER);
            // 获取id字段的值
            int id = resultSet.getInt(resultSet.getColumnIndexForName(AVStorage.Images.Media.ID));
            Uri uri = Uri.appendEncodedPathToUri(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI, String.valueOf(id));
            FileDescriptor fd = helper.openFile(uri, "r");
            ImageSource.DecodingOptions decodingOptions = new ImageSource.DecodingOptions();
            try {
                //解码并将图片放到image中
                imageSource = ImageSource.create(fd, null);
                pixelMap = imageSource.createPixelmap(null);
                int height = pixelMap.getImageInfo().size.height;
                int width = pixelMap.getImageInfo().size.width;
                float sampleFactor = Math.max(height /250f, width/250f);
                decodingOptions.desiredSize = new Size((int) (width/sampleFactor), (int)(height/sampleFactor));
                pixelMap = imageSource.createPixelmap(decodingOptions);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (imageSource != null) {
                    imageSource.release();
                }
            }
            image.setPixelMap(pixelMap);
            image.setClickedListener(new Component.ClickedListener() {
                @Override
                public void onClick(Component component) {
                    gotoCrop(uri);
                }
            });
            tableLayout.addComponent(image);
        }
    } catch (DataAbilityRemoteException | FileNotFoundException e) {
        e.printStackTrace();
    }
}
//uri放在intent中
private void gotoCrop(Uri uri){
    Intent intent = new Intent();
    intent.setUri(uri);
    present(new CropPicture(),intent);
}


HttpPictureChoose 的功能主要是将用户输入的网络图片地址解析为 Uri 传递给 CropPicture,目前只支持手动输入地址。


③MainMenu


一个简单的主菜单界面,用户可以通过点击不同的按钮选择裁剪相册图片还是网络图片。


03

Library 解析


鸿蒙和安卓存在较多的能力差异,即二者在实现同一 种功能时,方法不同,这不仅体现在工程结构上,也体现在具体的代码逻辑中。


以下将对 uCrop_ohos 和 uCrop 的工程结构进行对比,并介绍几个在 uCrop_ohos 移植过程中遇到的安卓和鸿蒙的能力差异。


①工程结构对比



图 6:uCrop_ohos(上)与 uCrop(下)的工程结构对比


上图可以看出 uCrop_ohos 相比 uCrop 少封装了一层 Activity 与 Fragment。


原因有 3 个:

  • 安卓的 Activity 与鸿蒙的 Ability 还是有差别的,强行复现会导致代码复用率低。

  • 这一层与 UI 强耦合,由于鸿蒙尚不支持安卓中许多控件,例如 Menu 等,这就导致难以原样复现 UCropActivity 中的 UI。

  • 封装程度越高,可供开发者自定义的程度就越小。


②能力差异


图片加载&保存:不论是加载网络图片还是相册图片,在 uCrop 和 uCrop_ohos 内部都是通过解析图片的 Uri 实现的,所以需要有一个识别 Uri 种类的过程,即通过分析 Uri 的 Scheme 来实现 Uri 的分类。


如果 Uri 的 Scheme 是 http 或 https 则会被认为是网络图片,调用 okhttp3 的能力执行下载操作。


如果 Uri 的 Scheme 是 content(安卓)或 dataability(鸿蒙)就会被认为是本地图片,执行复制操作。下载或复制的图片将作为被裁剪的图片。


代码如下所示:

private void processInputUri() throws NullPointerException, IOException {
    String inputUriScheme = mInputUri.getScheme();
    //Scheme为http或https即为网络图片,执行下载
    if ("http".equals(inputUriScheme) || "https".equals(inputUriScheme)) {
        try {
            downloadFile(mInputUri, mOutputUri);
        } catch (NullPointerException e) {
            LogUtils.LogError(TAG, "Downloading failed:"+e);
            throw e;
        }
    //安卓中Scheme为content即为本地图片,执行复制
    } else if ("content".equals(inputUriScheme)) {
        try {
            copyFile(mInputUri, mOutputUri);
        } catch (NullPointerException | IOException e) {
            LogUtils.LogError(TAG, "Copying failed:"+e);
            throw e;
        }
    //鸿蒙中Scheme为dataability即为本地图片,执行复制
    } else if("dataability".equals(inputUriScheme)){
        try {
            copyFile(mInputUri, mOutputUri);
        } catch (NullPointerException | IOException e) {
            LogUtils.LogError(TAG, "Copying failed:"+e);
            throw e;
        }


图片文件准备完成后,还需要将其解码成 Bitmap(安卓)或 PixelMap(鸿蒙)格式以便实现 uCrop 后续的各种功能。


在解码之前还需要通过 Uri 来获取文件流,在这一点上安卓和鸿蒙的实现原理不同。


对于安卓,可以通过 openInputStream() 函数获得输入文件流 InputStream:
InputStream stream = mContext.getContentResolver().openInputStream(mInputUri);


对于鸿蒙则需要调用 DataAbility,通过 DataAbilityHelper 先拿到 FileDescriptor,然后才能得到 InputStream:
InputStream stream = null;
DataAbilityHelper helper = DataAbilityHelper.creator(mContext);
FileDescriptor fd = helper.openFile(mInputUri, "r");
stream = new FileInputStream(fd);


同样地,对于图片保存需要的输出文件流 OutputStream,安卓和鸿蒙获取方式也存在不同。


具体代码如下:
//安卓获取OutputStream
outputStream = context.getContentResolver().openOutputStream(Uri.fromFile(new File(mImageOutputPath)));

//鸿蒙获取OutputStream
valuesBucket.putInteger("is_pending"1);
DataAbilityHelper helper = DataAbilityHelper.creator(mContext.get());
int id =helper.insert(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI, valuesBucket);
Uri uri = Uri.appendEncodedPathToUri(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI, String.valueOf(id));
//这里需要"w"写权限
FileDescriptor fd = helper.openFile(uri, "w");
OutputStream outputStream = new FileOutputStream(fd);


裁剪的实现:在安卓版的 uCrop 中,裁剪功能的实现原理是将原图(位图 1)位于裁剪框内的部分创建一个新的位图(位图 2),然后将新的位图保存成图片文件(图片文件 1)。


如图 7 所示:

图 7:uCrop 裁剪功能的实现方法


而在鸿蒙版 uCrop_ohos 中,裁剪功能的实现原理发生了变化。


鸿蒙系统 API 虽不支持对位图的旋转操作,但图像的解码 API 提供了旋转能力,所以鸿蒙的裁剪过程是这样的:


首先将原图(位图 1)保存为一个临时的图片文件(图片文件 1),通过相对旋转角度对临时图片文件进行读取,此时读取出的位图(位图 2)就包含了正确的旋转信息。


然后再通过相对缩放和位移创建一个新的位图(位图 3),这个位图还会因为 API 的特性发生压缩和错切等形变,所以还需要再创建最后一个位图(位图 4)来修正形变,最后再将位图4保存成图片文件(图片文件 2)。


如图 8 所示:

图 8:uCrop_ohos 裁剪功能的实现方法


异步任务处理:由于图片的读取、裁剪和保存这些操作都是比较消耗系统性能的,直接导致的问题就是卡顿。


所以需要使用异步任务将这些操作放到后台操作,减少 UI 线程的负担。下面以裁剪任务为例进行介绍。


在 uCrop 中使用的是 BitmapCropTask 类继承 AsyncTask 类的方法:
public class BitmapCropTask extends AsyncTask<VoidVoidThrowable>


然后在其中重写 doInBackground() 和 onPostExecute() 函数,分别实现后台裁剪任务的处理与回调:
@Override
@Nullable
protected Throwable doInBackground(Void... params) {
    if (mViewBitmap == null) {
        return new NullPointerException("ViewBitmap is null");
    } else if (mViewBitmap.isRecycled()) {
        return new NullPointerException("ViewBitmap is recycled");
    } else if (mCurrentImageRect.isEmpty()) {
        return new NullPointerException("CurrentImageRect is empty");
    }

    try {
        crop();
        mViewBitmap = null;
    } catch (Throwable throwable) {
        return throwable;
    }

    return null;
}
@Override
protected void onPostExecute(@Nullable Throwable t) {
    if (mCropCallback != null) {
        if (t == null) {
            Uri uri = Uri.fromFile(new File(mImageOutputPath));
            mCropCallback.onBitmapCropped(uri, cropOffsetX, cropOffsetY, mCroppedImageWidth, mCroppedImageHeight);
        } else {
            mCropCallback.onCropFailure(t);
        }
    }
}


鸿蒙中没有搭载类似安卓的 AsyncTask 类,所以 uCrop_ohos 修改了后台任务的处理方案。


首先将后台任务的处理与回调合并写在一个 Runnable 中,然后鸿蒙原生的多线程处理机制 EventHandler 搭配 EventRunner 新开一个线程用于处理这个 Runnable,实现了图片裁剪任务的异步处理。
public void doInBackground(){                  
    EventRunner eventRunner = EventRunner.create();
    EventHandler handler = new EventHandler(eventRunner);
    handler.postTask(new Runnable() {
        @Override
        public void run() {
            if (mViewBitmap == null) {
                Throwable t = new NullPointerException("ViewBitmap is null");
                mCropCallback.onCropFailure(t);
                return;
            } else if (mViewBitmap.isReleased()) {
                Throwable t = new NullPointerException("ViewBitmap is null");
                mCropCallback.onCropFailure(t);
                return;
            } else if (mCurrentImageRect.isEmpty()) {
                Throwable t = new NullPointerException("ViewBitmap is null");
                mCropCallback.onCropFailure(t);
                return;
            }
            try {
                crop();
                mViewBitmap = null;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    });
}


项目贡献人:吴圣垚、郑森文朱伟陈美汝王佳思


👇点击关注鸿蒙技术社区👇

专注开源技术,共建鸿蒙生态


“阅读原文”了解更多

文章转载自鸿蒙技术社区,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论