前言
基于安卓平台的图片裁切组件crop_image_layout(https://github.com/yulu/crop-image-layout ),实现了鸿蒙化迁移和重构,代码已经开源到(https://gitee.com/isrc_ohos/crop_image_layout_ohos ),目前已经获得了很多人的Star和Fork ,欢迎各位下载使用并提出宝贵意见!
背景
crop_image_layout_ohos组件能对图片进行旋转和自定义裁切的操作,并且无论待裁切图片原尺寸有多大或多小,最终都将在以最佳尺寸在组件内显示。同时,该组件操作界面简洁且使用方法简单,易被开发者使用或优化,能够提升应用的丰富性和可操作性。
组件效果展示
组件中可以通过操作图片、裁切框、按钮,最终实现在图片中裁切部分区域并进行显示的效果,组件的运行效果如图1所示。
图1 crop_image_layout_ohos组件的运行效果图
对应运行效果图,详细解释其主要提供的功能:
- 点击“rotate”按钮可以对图片进行旋转操作。
- 手指按住裁切框内任意处并拖动,可实现裁切框移动,裁切框停止移动时,框内的图片即为想要裁切的图片;
- 被选中区域的左上角和右下角坐标会在图片下方的文本框中进行显示;
- 点击“crop”按钮可对选中的图片区域进行裁切,之后会跳转到第二个界面显示裁切后的图片;
Sample解析
1.组件的整体使用流程
图2 组件使用流程示意图
在介绍组件的使用前,先来介绍下构成crop_image_layout_ohos组件功能的3个重要部分:
-裁切框:负责划定图片的裁切区域;
-裁切图片:是指被导入组件中,即将被裁切的图片;
-组件区域:是指组件所在的位置;
组件的使用过程可以概括为首先设置裁切框的位置(坐标),然后将裁切框坐标数据添加到裁切图片中,最后将裁切框和裁切图片添加到组件区域中。
在这过程中,组件还实现了对裁切图片和裁切框尺寸的适配显示效果,这部分的具体原理会在Library解析部分进行讲解。
2.组件的具体使用步骤
下面介绍crop_image_layout_ohos组件的具体使用方法,共分为6个步骤:
步骤1. 在xml文件中添加EditPhotoView控件。
步骤2. 导入所需类并实例化类对象。
步骤3. 将裁切框坐标数据设置到裁切图片。
步骤4. 将裁切图片和裁切框添加到布局中
步骤5. 显示裁切框左上角和右下角坐标值。
步骤6. 设置监听事件。
(1)在xml文件中添加EditPhotoView控件
在xml文件中以com.huawei.croplayout.EditPhotoView为控件名添加EditPhotoView控件,用来显示crop_image_layout_ohos的组件区域。并分别设置裁切框拐角和边的颜色以及裁切图片未被选中部分的阴影颜色,如图所示。
图3 属性设置示意图
- <com.huawei.croplayout.EditPhotoView//添加组件区域
- ohos:id="$+id:editable_image"
- ...
- crop:crop_corner_color="#45B4CA"//裁切框拐角颜色
- crop:crop_line_color="#d7af55" //裁切框边颜色
- crop:crop_shadow_color="#77ffffff"/> //裁切图片未被选中部分的阴影颜色
(2)导入所需类并实例化类对象
在MainAbilitySlice类的onStart()方法中,分别导入类onBoxChangedListeneron、EditPhotoView、EdittableImage、ScalableBox。
- BoxChangedListener类用于监听裁切框变化;
- EditPhotoView类用于设置组件区域;
- EdittableImage类用于设置裁切图片;
- ScalableBox类用于设置裁切框;
- import com.example.croplayout.handler.OnBoxChangedListener;//裁切框变化监听
- import com.example.croplayout.EditPhotoView;//组件区域
- import com.example.croplayout.EditableImage;//裁切图片
- import com.example.croplayout.model.ScalableBox;//裁切框
创建EditPhotoView类和Text类对象分别用于绑定crop_image_layout_ohos的组件区域和用于显示裁切框坐标的文本控件;实例化EdittableImage类对象image,使其包含图1所示的裁切图片,实现组件裁切图片的导入。创建一个元素类型为ScalableBox的List,并将其命名为boxes,用于盛纳裁切框的坐标。新实例化一个左上角坐标为(25,180)、右下角坐标为(640,880)的裁切框对象,并调用add()方法将其添加到上述boxes中。
- //用于绑定组件的裁切图片视图区域
- final EditPhotoView imageView = (EditPhotoView) findComponentById(ResourceTable.Id_editable_image);
- final Text boxText = (Text) findComponentById(ResourceTable.Id_box_text);
- final EditableImage image = new EditableImage(this, ResourceTable.Media_photo2);
- List<ScalableBox> boxes = new ArrayList<>();//用于设置裁切框的坐标
- boxes.add(new ScalableBox(25, 180, 640, 880));//裁切框的坐标
(3)将裁切框坐标数据设置到裁切图片
通过setBoxes()方法将boxes中裁切框对象的坐标数据设置到裁切图片image中,实现裁切框相对裁切图片的位置设定;
- image.setBoxes(boxes);
(4)将裁切图片和裁切框添加到布局中
调用intView()方法,创建裁切图片和裁切框的视图,并将其添加到组件布局中进行显示。
- imageView.initView(this, image);
(5)显示裁切框左上角和右下角坐标值
重新声明一个ScalableBox类型的对象activeBox,用于动态取裁切框的坐标,并将其通过Text在界面上显示出来。
- ScalableBox activeBox = image.getActiveBox();//动态获取图片中裁切框选取区域的坐标
- boxText.setText("box: [" + activeBox.getX1() + "," + activeBox.getY1() +
- "],[" + activeBox.getX2() + "," + activeBox.getY2() + "]");
(6)设置监听事件
组件区域监听事件
为组件区域对象imageView设置监听事件,当裁切框位置发生变化时,将其坐标设置到Text对象boxText中进行显示。
- imageView.setOnBoxChangedListener(new OnBoxChangedListener() {
- @Override//设置裁切框区域监听事件
- public void onChanged(int x1, int y1, int x2, int y2) {
- boxText.setText("box: [" + x1 + "," + y1 + "],[" + x2 + "," + y2 + "]");
- }
- });
旋转按钮“rotate”监听事件
声明一个Button类对象rotateButton将其与“rotate_button”控件绑定。为其设置点击监听事件,按钮被点击时,通过组件区域对象imageView调用rotateImageView()方法实现裁切图片向右旋转90°的效果。
- Button rotateButton = (Button) findComponentById(ResourceTable.Id_rotate_button);//与”rorate_button“控件绑定
- rotateButton.setClickedListener(new Component.ClickedListener() {
- @Override//设置点击监听事件
- public void onClick(Component component) {
- imageView.rotateImageView();//实现裁切图片向右旋转90°
- }
- });
裁切按钮“crop”监听事件
声明一个Button类对象cropButton将其与“crop_button”控件绑定,并设置点击监听事件。按钮被点击时,通过裁切图片image调用cropOriginalImage()方法得到裁切后的图片并将其存放于PixelMap类对象中;通过Intent跳转到第二个界面,并将裁切后的图片作为参数传入,显示在第二个界面中。
- Button cropButton = (Button) findComponentById(ResourceTable.Id_crop_button);
- cropButton.setClickedListener(new Component.ClickedListener() {
- @Override//设置点击监听事件
- public void onClick(Component component) {
- PixelMap croppedImage = image.cropOriginalImage();
- Intent newIntent = new Intent();
- newIntent.setParam("image", croppedImage);
- present(new SecondAbilitySlice(), newIntent);
- }
- });
Library解析
- Library部分将围绕图2,对crop_image_layout_ohos组件的原理和执行逻辑进行梳理。其中会涉及到ScalableBox类、EdittableImage类、EditPhotoView类、SelectionView类、和ImageHelper类;
- ScalableBox类、EdittableImage类、EditPhotoView类在上一节中简单介绍过是用来设置组件区域、裁切图片和裁切框的;
- SelectionView类用于用于设置裁剪框所在的视图;
- ImageHelper类相当于一个图片操作辅助工具,用来完成从原裁剪图片中获取PixelMap和旋转图片等图片处理操作。
1.设置裁切框(实例化其尺寸)
图4 裁切框坐标示意图
在Sample解析中我们讲过,需要调用add()方法将新实例化的左上角坐标为(25,180)、右下角坐标为(640,880)的裁切框加入到boxes对象中。其中,左上角坐标对应图4中的(X1,Y1),右上角对应图4中的(X2,Y2),通过设置裁切框对角线上两个点,就可以唯一确定其大小和位置了。
- boxes.add(new ScalableBox(25, 180, 640, 880));
实例化过程需要通过ScalableBox类的构造函数,设置裁切框左上角和右下角的坐标。
- public ScalableBox(int x1, int y1, int x2, int y2) {
- this.x1 = x1;
- this.y1 = y1;
- this.x2 = x2;
- this.y2 = y2;
- }
2.将裁切框坐标数据设置到裁切图片
获取裁切框列表boxes的第一个对象,即我们之前设置好对角线坐标的裁切框数据,并将其添加到裁切图片中。这是通过EditableImage类的setBoxes()方法实现的。
在该方法中,若裁切框对象列表boxes不为空且尺寸大于0,则将boxes赋给EditableImage类的成员变量originalBoxes,用于存储所有的裁剪框对象的数据;将另一个此类的成员变量copyofActiveBox实例化,用于存储被选中的裁剪框两个角的坐标值;其中,activeBoxIdx是指在boxes中盛纳裁切框坐标的下标,List可以为用户预留多个裁切框坐标,本组件中只是用下表为0的裁切框坐标。
- public void setBoxes(List<ScalableBox> boxes, int activeBoxIdx) {
- if (boxes != null && boxes.size() > 0) {//如果boxes对象不为空且尺寸大于0
- this.originalBoxes = boxes;
- copyOfActiveBox = new ScalableBox();
- copyOfActiveBox.setX1(originalBoxes.get(activeBoxIdx).getX1());
- copyOfActiveBox.setX2(originalBoxes.get(activeBoxIdx).getX2());
- copyOfActiveBox.setY1(originalBoxes.get(activeBoxIdx).getY1());
- copyOfActiveBox.setY2(originalBoxes.get(activeBoxIdx).getY2());
- }
- }
3.将裁切图片和裁剪框添加到布局中
该功能是通过EditPhotoView类的initView()方法实现的。先根据创建EditPhotoView对象时传入的裁切框尺寸、边角尺寸和颜色等属性,将SelectionView类实例化,用于展示裁剪框的视图;然后根据传入的裁切图片实例化得到Image图片类对象,用于展示裁切图片的视图;再将SelectionView类对象和Image对象的布局设置为跟随父组件,并将两者添加到裁切组件区域布局中,实现裁切框和裁切图片的显示。
分别通过setViewSize()方法设置裁切图片视图区域尺寸、setPixelMap()为其设置裁切图片位图格式、setScaleMode()方法为其设置图片缩放模式为中心缩放、setBoxSize()方法设置裁切图片和裁切框适配后的尺寸。
- public void initView(Context context, EditableImage editableImage) {
- this.editableImage = editableImage;
- selectionView = new SelectionView(context,
- lineWidth, cornerWidth, cornerLength,
- lineColor, cornerColor, dotColor, shadowColor, editableImage);
- imageView = new Image(context);//设置选择区域尺寸、边角尺寸以及颜色
- imageView.setLayoutConfig(new LayoutConfig(LayoutConfig.MATCH_PARENT, LayoutConfig.MATCH_PARENT));//跟随父组件
- selectionView.setLayoutConfig(new LayoutConfig(LayoutConfig.MATCH_PARENT, LayoutConfig.MATCH_PARENT));
- addComponent(imageView, 0);//将裁切框区域和选择区域添加到布局中
- addComponent(selectionView, 1);
- if (editableImage != null) {
- editableImage.setViewSize(mWidth, mHeight);
- imageView.setPixelMap(editableImage.getOriginalPixelMap());
- imageView.setScaleMode(Image.ScaleMode.ZOOM_CENTER);//中心缩放模式
- selectionView.setBoxSize(editableImage, editableImage.getBoxes(), mWidth, mHeight);
- }
- }
(1)在裁切图片视图区域中适配裁切图片
由于裁切图片的视图区域与组件区域的大小相同,二裁切图片的大小是不固定的,因此裁切图片在显示到其视图区域时,需进行尺寸的适配。
上述功能是由EditableImage类的getFitSize()方法提供,该方法在上述setBoxSize()方法中被调用。通过该方法能够将裁切图片与其视图区域进行适配,同时返回适配后图片尺寸。这是为了更好地在裁切图片视图区域中展示图片,无论过大或过小尺寸的图片都能在此区域中被缩放至最合适的程度显示。原理可参考图5。
图5 适配图片尺寸原理图(左:ratio>viewRatio,右反之)
先计算原裁切图片(即粉色矩形)宽和高的比值ratio(即a/b)和组件区域(即黄色矩形)宽和高的比值viewRatio(即c/d)。
判断若原裁切图片宽高比大于裁切图片视图区域宽高比即图5中左图的情况,则说明可以将原裁切图片最大程度放大至宽a与裁切图片视图区域宽c长度一致的尺寸(即蓝色矩形),此时原裁切图片高b按ratio放大后的长度一定小于裁切图片视图区域高d,因此可以根据图片宽放大的倍数factor求出放大后高的长度。
若原裁切图片宽高比小于裁切图片视图区域宽高比即图5中右图的情况,与上一种情况同理,则说明可以将原裁切图片最大程度放大至高b与裁切图片视图区域高d长度一致的尺寸(即蓝色矩形),此时原裁切图片宽a按ratio放大后的长度一定小于裁切图片视图区域宽c,因此可以根据图片高放大的倍数factor求出放大后宽的长度。
计算完成后将图片放大后的宽和高分别存放在int型数组fitSize[]中。上述是以原裁切图片尺寸小于裁切图片视图区域为例,反之同理。
- public int[] getFitSize() {//适配图片,将图片缩放至比例与裁切图片视图区域比例一致
- int[] fitSize = new int[2];//用于存放适配后的图片宽高
- //原裁剪图片宽高比
- float ratio = originalPixelMap.getImageInfo().size.width / (float) originalPixelMap.getImageInfo().size.height;
- float viewRatio = viewWidth / (float) viewHeight;//裁切图片视图区域宽高比
- //原裁剪图片宽和高比例大于裁切图片视图区域宽和高比例
- if (ratio > viewRatio) {
- float factor = viewWidth / (float) originalPixelMap.getImageInfo().size.width;//裁切图片宽放大的倍数
- fitSize[0] = viewWidth;//宽为裁切图片视图区域宽
- fitSize[1] = (int) (originalPixelMap.getImageInfo().size.height * factor);//根据宽放大的倍数计算放大后高的长度
- } else { //原裁剪图片宽和高比例小于裁切图片视图区域宽和高比例
- float factor = viewHeight / (float) originalPixelMap.getImageInfo().size.height;
- fitSize[0] = (int) (originalPixelMap.getImageInfo().size.width * factor);
- fitSize[1] = viewHeight;
- }
- return fitSize;
- }
(2)将裁切框和图片进行适配
裁切框需要与裁切图片保持相同的显示比例,因此裁切框需要和裁切图片进行适配。
上述功能是由SelectionView类的setBoxsize()方法。获取适配后图片的宽高,与裁切框宽高进行计算得到originX和originY,并调用setDisplayBoxes()方法设置适配后裁切框的坐标。
- public void setBoxSize(EditableImage editableImage, List<ScalableBox> originalBoxes, int widthX, int heightY) {
- int[] fitSize = editableImage.getFitSize();//获取前面计算地适配后的图片尺寸
- this.pixelMapWidth = fitSize[0];//适配后图片的宽
- this.pixelMapHeight = fitSize[1];//适配后图片地高
- int originX = (widthX - pixelMapWidth) / 2;
- int originY = (heightY - pixelMapHeight) / 2;
- this.originX = originX;
- this.originY = originY;
- setDisplayBoxes(originalBoxes);//设置适配后裁切框的坐标
- invalidate();
- }
setDisplayBoxes()方法中核心部分是根据图片缩放比例计算适配后裁切框对角线上两点的坐标。先计算图片缩放前后宽的比值scale(即c/a),用之前实例化时设置的裁切框初始尺寸的左上角横坐标X1与缩放比例scale相乘得到适配后的横坐标,再加上前面计算好的originX,即得到适配后的裁切框左上角横坐标scaleX1,右下角横坐标scaleX2、左上角竖坐标scaleY1、右下角竖坐标scaleY2同理。
- float scale = ((float) editableImage.getFitSize()[0]) / editableImage.getActualSize()[0];
- int scaleX1 = (int) Math.ceil((originalBox.getX1() * scale) + originX);
- int scaleX2 = (int) Math.ceil((originalBox.getX2() * scale) + originX);
- int scaleY1 = (int) Math.ceil((originalBox.getY1() * scale) + originY);
- int scaleY2 = (int) Math.ceil((originalBox.getY2() * scale) + originY);
- //将适配后的裁切框重新加入到裁切图片中
- displayBox.setX1(scaleX1);
- displayBox.setX2(scaleX2);
- displayBox.setY1(scaleY1);
- displayBox.setY2(scaleY2);