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

开源
文章由鸿蒙社区产出,想要了解更多内容请前往:51CTO和华为官方战略合作共建的鸿蒙技术社区https://harmonyos.51cto.com

[[391927]]

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

前言

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

背景

uCrop组件是开源的图片裁剪库,支持对图片的缩放和裁剪等操作,是安卓平台比较受欢迎的组件,在Github上已有1万多个Star和近2千个Fork。uCrop组件具有封装程度高、使用流畅、自定义程度高的优点,被广泛应用于多种APP中。

组件效果展示

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

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

1、uCrop_ohos图片选择

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

图 1 主菜单界面

(1)uCrop_ohos读取相册图片

当用户赋予组件相应权限后,uCrop_ohos可以自动读取手机相册中每一张图片,并将它们的缩略图作为一个列表呈现在UI界面上,用户可以上下滑动列表寻找目标图片,如图2所示。当用户点击某张缩略图时,会跳转到uCrop_ohos的裁剪界面,执行后续操作。

图 2 选择系统相册图片

(2)uCrop_ohos读取网络图片

用户需要将图片网址键入到输入框内并点击确定按钮,如图3所示。uCrop_ohos会自动下载图片并跳转到裁剪界面,执行后续操作。

图 3 选择网络图片

2、uCrop_ohos图片裁剪

图4 uCrop_ohos的裁剪界面

图4是uCrop_ohos的裁剪界面。使用者可以通过手势对图片进行缩放、旋转和平移的操作,也可以通过按钮、滑块等控件进行相应操作。将图片调整至满意状态时,点击裁剪按钮即可获得裁剪后的新图片,并将其保存至手机相册。且本组件的图片与裁剪框具有自适应能力,能够保证裁剪框时刻在图片范围内,防止由于裁剪框的范围大于图片导致的一系列问题。

Sample解析

图5 Sample的工程结构

uCrop_ohos的核心能力都由其Library提供,Sample主要用于构建UI,并调用Library的接口。从图5可以看出Sample的工程结构较为简单,主要由4个文件构成,下面进行详细的介绍。

1、CropPicture

CropPicture文件提供了裁剪界面,其最主要的逻辑是通过图片Uri实例化Library中UCropView类。由于uCrop_ohos的逻辑是先将用户选择的原图创建一个副本,然后对副本执行裁剪,所以为了将图片传入UCropView需要两个Uri:一个名为uri_i,从intent中获得,标识的是用户选择的原图,可以是本地图片也可以是网络图片;另一个名为uri_o,标识的是原图副本,一定是一张本地图片。代码如下:

  1. //URI_IN 
  2. Uri uri_i = intent.getUri(); 
  3.  
  4. //URI_OUT 
  5. String filename = "test.jpg"
  6. PixelMap.InitializationOptions options = new PixelMap.InitializationOptions(); 
  7. options.size = new Size(100,100); 
  8. PixelMap pixelmap = PixelMap.create(options); 
  9. Uri uri_o = saveImage(filename, pixelmap); 
  10.  
  11. //UcropView 
  12. UCropView uCropView = new UCropView(this); 
  13. try { 
  14.     uCropView.getCropImageView().setImageUri(uri_i, uri_o); 
  15.     uCropView.getOverlayView().setShowCropFrame(true); 
  16.     uCropView.getOverlayView().setShowCropGrid(true); 
  17.     uCropView.getOverlayView().setDimmedColor(Color.TRANSPARENT.getValue()); 
  18.  
  19. } catch (Exception e) { 
  20.     e.printStackTrace(); 

Library给开发者提供了public接口,使得开发者易于封装自己的UI功能。例如本文件中的旋转和缩放滑块、旋转和缩放按钮、当前旋转和缩放状态的显示都是调用Library接口实现的。以如下功能的实现为例:创建了一个按钮,当用户触碰这个按钮之后就可以将图片右旋90度。其核心能力就是依靠调用Library中postRotate()函数实现的,非常简单。

  1. //右旋90度的Button 
  2. Button button_plus_90 = new Button(this); 
  3. button_plus_90.setText("+90°"); 
  4. button_plus_90.setTextSize(80); 
  5. button_plus_90.setBackground(buttonBackground); 
  6. button_plus_90.setClickedListener(new Component.ClickedListener() { 
  7.     @Override 
  8.     public void onClick(Component component) { 
  9.         float degrees = 90f; 
  10.         //计算旋转中心 
  11.         float center_X = uCropView.getOverlayView().getCropViewRect().getCenter().getPointX(); 
  12.         float center_Y = uCropView.getOverlayView().getCropViewRect().getCenter().getPointY(); 
  13.         //旋转 
  14.         uCropView.getCropImageView().postRotate(degrees,center_X,center_Y); 
  15.         //适配 
  16.         uCropView.getCropImageView().setImageToWrapCropBounds(false); 
  17.         //显示旋转角度 
  18.         mDegree = uCropView.getCropImageView().getCurrentAngle(); 
  19.         text.setText("当前旋转角度: " + df.format(mDegree) + " °"); 
  20.     } 
  21. }); 

2、LocalPictureChoose & HttpPictureChoose

由上文可知,uri_i是通过intent得到的,这个intent就是由 LocalPictureChoose或HttpPictureChoose传递的。LocalPictureChoose提供选择相册图片的能力,HttpPictureChoose提供选择网络图片的能力。

LocalPictureChoose提供的功能是将相册中的全部图片读取出来,做成缩略图排列在UI上,然后将每个缩略图绑定一个触摸监听器,一旦使用者选中某个缩略图,就会将这个缩略图对应的原图uri放在intent中传给CropPicture。具体代码如下:

  1. private void showImage() { 
  2.     DataAbilityHelper helper = DataAbilityHelper.creator(this); 
  3.     try { 
  4.         // columns为null,查询记录所有字段,当前例子表示查询id字段 
  5.         ResultSet resultSet = helper.query(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI, new String[]{AVStorage.Images.Media.ID}, null); 
  6.         while (resultSet != null && resultSet.goToNextRow()) { 
  7.             //创建image用以显示系统相册缩略图 
  8.             PixelMap pixelMap = null
  9.             ImageSource imageSource = null
  10.             Image image = new Image(this); 
  11.             image.setWidth(250); 
  12.             image.setHeight(250); 
  13.             image.setMarginsLeftAndRight(10, 10); 
  14.             image.setMarginsTopAndBottom(10, 10); 
  15.             image.setScaleMode(Image.ScaleMode.CLIP_CENTER); 
  16.             // 获取id字段的值 
  17.             int id = resultSet.getInt(resultSet.getColumnIndexForName(AVStorage.Images.Media.ID)); 
  18.             Uri uri = Uri.appendEncodedPathToUri(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI, String.valueOf(id)); 
  19.             FileDescriptor fd = helper.openFile(uri, "r"); 
  20.             ImageSource.DecodingOptions decodingOptions = new ImageSource.DecodingOptions(); 
  21.             try { 
  22.                 //解码并将图片放到image中 
  23.                 imageSource = ImageSource.create(fd, null); 
  24.                 pixelMap = imageSource.createPixelmap(null); 
  25.                 int height = pixelMap.getImageInfo().size.height; 
  26.                 int width = pixelMap.getImageInfo().size.width; 
  27.                 float sampleFactor = Math.max(height /250f, width/250f); 
  28.                 decodingOptions.desiredSize = new Size((int) (width/sampleFactor), (int)(height/sampleFactor)); 
  29.                 pixelMap = imageSource.createPixelmap(decodingOptions); 
  30.             } catch (Exception e) { 
  31.                 e.printStackTrace(); 
  32.             } finally { 
  33.                 if (imageSource != null) { 
  34.                     imageSource.release(); 
  35.                 } 
  36.             } 
  37.             image.setPixelMap(pixelMap); 
  38.             image.setClickedListener(new Component.ClickedListener() { 
  39.                 @Override 
  40.                 public void onClick(Component component) { 
  41.                     gotoCrop(uri); 
  42.                 } 
  43.             }); 
  44.             tableLayout.addComponent(image); 
  45.         } 
  46.     } catch (DataAbilityRemoteException | FileNotFoundException e) { 
  47.         e.printStackTrace(); 
  48.     } 
  49. //uri放在intent中 
  50. private void gotoCrop(Uri uri){ 
  51.     Intent intent = new Intent(); 
  52.     intent.setUri(uri); 
  53.     present(new CropPicture(),intent); 

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

3、MainMenu

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

Library解析

鸿蒙和安卓存在较多的能力差异,即二者在实现同一 种功能时,方法不同,这不仅体现在工程结构上,也体现在具体的代码逻辑中。以下将对uCrop_ohos和uCrop的工程结构进行对比,并介绍几个在uCrop_ohos移植过程中遇到的安卓和鸿蒙的能力差异。

1、工程结构对比

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

可以看出uCrop_ohos相比uCrop少封装了一层Activity与Fragment,原因有3个:

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

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

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

2、能力差异

(1)图片加载&保存

不论是加载网络图片还是相册图片,在uCrop和uCrop_ohos内部都是通过解析图片的Uri实现的,所以需要有一个识别Uri种类的过程,即通过分析Uri的Scheme来实现Uri的分类。如果Uri的Scheme是http或https则会被认为是网络图片,调用okhttp3的能力执行下载操作;如果Uri的Scheme是content(安卓)或dataability(鸿蒙)就会被认为是本地图片,执行复制操作。下载或复制的图片将作为被裁剪的图片。代码如下所示:

  1. private void processInputUri() throws NullPointerException, IOException { 
  2.     String inputUriScheme = mInputUri.getScheme(); 
  3.     //Scheme为http或https即为网络图片,执行下载 
  4.     if ("http".equals(inputUriScheme) || "https".equals(inputUriScheme)) { 
  5.         try { 
  6.             downloadFile(mInputUri, mOutputUri); 
  7.         } catch (NullPointerException e) { 
  8.             LogUtils.LogError(TAG, "Downloading failed:"+e); 
  9.             throw e; 
  10.         } 
  11.     //安卓中Scheme为content即为本地图片,执行复制 
  12.     } else if ("content".equals(inputUriScheme)) { 
  13.         try { 
  14.             copyFile(mInputUri, mOutputUri); 
  15.         } catch (NullPointerException | IOException e) { 
  16.             LogUtils.LogError(TAG, "Copying failed:"+e); 
  17.             throw e; 
  18.         } 
  19.     //鸿蒙中Scheme为dataability即为本地图片,执行复制 
  20.     } else if("dataability".equals(inputUriScheme)){ 
  21.         try { 
  22.             copyFile(mInputUri, mOutputUri); 
  23.         } catch (NullPointerException | IOException e) { 
  24.             LogUtils.LogError(TAG, "Copying failed:"+e); 
  25.             throw e; 
  26.         } 

图片文件准备完成后,还需要将其解码成Bitmap(安卓)或PixelMap(鸿蒙)格式以便实现uCrop后续的各种功能。在解码之前还需要通过Uri来获取文件流,在这一点上安卓和鸿蒙的实现原理不同。对于安卓,可以通过openInputStream()函数获得输入文件流InputStream:

  1. InputStream stream = mContext.getContentResolver().openInputStream(mInputUri); 

对于鸿蒙则需要调用DataAbility,通过DataAbilityHelper先拿到FileDescriptor,然后才能得到InputStream:

  1. InputStream stream = null
  2. DataAbilityHelper helper = DataAbilityHelper.creator(mContext); 
  3. FileDescriptor fd = helper.openFile(mInputUri, "r"); 
  4. stream = new FileInputStream(fd); 

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

  1. //安卓获取OutputStream 
  2. outputStream = context.getContentResolver().openOutputStream(Uri.fromFile(new File(mImageOutputPath))); 
  3.  
  4. //鸿蒙获取OutputStream 
  5. valuesBucket.putInteger("is_pending", 1); 
  6. DataAbilityHelper helper = DataAbilityHelper.creator(mContext.get()); 
  7. int id =helper.insert(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI, valuesBucket); 
  8. Uri uri = Uri.appendEncodedPathToUri(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI, String.valueOf(id)); 
  9. //这里需要"w"写权限 
  10. FileDescriptor fd = helper.openFile(uri, "w"); 
  11. OutputStream outputStream = new FileOutputStream(fd); 

(2)裁剪的实现

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

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

而在鸿蒙版uCrop_ohos中,裁剪功能的实现原理发生了变化。鸿蒙系统API虽不支持对位图的旋转操作,但图像的解码API提供了旋转能力,所以鸿蒙的裁剪过程是这样的:

首先将原图(位图1)保存为一个临时的图片文件(图片文件1),通过相对旋转角度对临时图片文件进行读取,此时读取出的位图(位图2)就包含了正确的旋转信息。然后再通过相对缩放和位移创建一个新的位图(位图3),这个位图还会因为API的特性发生压缩和错切等形变,所以还需要再创建最后一个位图(位图4)来修正形变,最后再将位图4保存成图片文件(图片文件2)。如图8所示:

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

(3)异步任务处理

由于图片的读取、裁剪和保存这些操作都是比较消耗系统性能的,直接导致的问题就是卡顿,所以需要使用异步任务将这些操作放到后台操作,减少UI线程的负担。下面以裁剪任务为例进行介绍。

在uCrop中使用的是BitmapCropTask类继承AsyncTask类的方法:

  1. public class BitmapCropTask extends AsyncTask<Void, Void, Throwable> 

然后在其中重写doInBackground()和onPostExecute()函数,分别实现后台裁剪任务的处理与回调:

  1. @Override 
  2. @Nullable 
  3. protected Throwable doInBackground(Void... params) { 
  4.     if (mViewBitmap == null) { 
  5.         return new NullPointerException("ViewBitmap is null"); 
  6.     } else if (mViewBitmap.isRecycled()) { 
  7.         return new NullPointerException("ViewBitmap is recycled"); 
  8.     } else if (mCurrentImageRect.isEmpty()) { 
  9.         return new NullPointerException("CurrentImageRect is empty"); 
  10.     } 
  11.  
  12.     try { 
  13.         crop(); 
  14.         mViewBitmap = null
  15.     } catch (Throwable throwable) { 
  16.         return throwable; 
  17.     } 
  18.  
  19.     return null
  20. @Override 
  21. protected void onPostExecute(@Nullable Throwable t) { 
  22.     if (mCropCallback != null) { 
  23.         if (t == null) { 
  24.             Uri uri = Uri.fromFile(new File(mImageOutputPath)); 
  25.             mCropCallback.onBitmapCropped(uri, cropOffsetX, cropOffsetY, mCroppedImageWidth, mCroppedImageHeight); 
  26.         } else { 
  27.             mCropCallback.onCropFailure(t); 
  28.         } 
  29.     } 

鸿蒙中没有搭载类似安卓的AsyncTask类,所以uCrop_ohos修改了后台任务的处理方案,首先将后台任务的处理与回调合并写在一个Runnable中,然后鸿蒙原生的多线程处理机制EventHandler搭配EventRunner新开一个线程用于处理这个Runnable,实现了图片裁剪任务的异步处理。

  1. public void doInBackground(){                   
  2.     EventRunner eventRunner = EventRunner.create(); 
  3.     EventHandler handler = new EventHandler(eventRunner); 
  4.     handler.postTask(new Runnable() { 
  5.         @Override 
  6.         public void run() { 
  7.             if (mViewBitmap == null) { 
  8.                 Throwable t = new NullPointerException("ViewBitmap is null"); 
  9.                 mCropCallback.onCropFailure(t); 
  10.                 return
  11.             } else if (mViewBitmap.isReleased()) { 
  12.                 Throwable t = new NullPointerException("ViewBitmap is null"); 
  13.                 mCropCallback.onCropFailure(t); 
  14.                 return
  15.             } else if (mCurrentImageRect.isEmpty()) { 
  16.                 Throwable t = new NullPointerException("ViewBitmap is null"); 
  17.                 mCropCallback.onCropFailure(t); 
  18.                 return
  19.             } 
  20.             try { 
  21.                 crop(); 
  22.                 mViewBitmap = null
  23.             } catch (IOException e) { 
  24.                 e.printStackTrace(); 
  25.             } 
  26.         } 
  27.     }); 

项目贡献人

吴圣垚 郑森文 朱伟 陈美汝 王佳思

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

 

责任编辑:jianghua 来源: 鸿蒙社区
相关推荐

2021-03-03 09:42:26

鸿蒙HarmonyOS图片裁剪

2021-06-17 14:56:00

鸿蒙HarmonyOS应用

2021-11-17 15:37:43

鸿蒙HarmonyOS应用

2021-10-19 10:04:51

鸿蒙HarmonyOS应用

2021-08-10 15:23:08

鸿蒙HarmonyOS应用

2021-07-06 18:21:31

鸿蒙HarmonyOS应用

2021-04-20 15:06:42

鸿蒙HarmonyOS应用

2021-08-30 17:55:58

鸿蒙HarmonyOS应用

2021-03-10 15:03:40

鸿蒙HarmonyOS应用

2021-04-29 14:32:24

鸿蒙HarmonyOS应用

2021-04-15 17:47:38

鸿蒙HarmonyOS应用

2021-07-20 15:20:40

鸿蒙HarmonyOS应用

2021-11-02 14:54:21

鸿蒙HarmonyOS应用

2021-08-03 10:07:41

鸿蒙HarmonyOS应用

2021-06-29 09:28:16

鸿蒙HarmonyOS应用

2021-03-24 09:30:49

鸿蒙HarmonyOS应用

2021-07-28 09:40:04

鸿蒙HarmonyOS应用

2021-08-26 16:07:46

鸿蒙HarmonyOS应用

2021-03-01 14:00:11

鸿蒙HarmonyOS应用

2021-01-27 10:04:46

鸿蒙HarmonyOS动画
点赞
收藏

51CTO技术栈公众号