基于安卓平台的图片裁切组件 crop_image_layout,实现了鸿蒙化迁移和重构,代码已经开源,目前已经获得了很多人的 Star 和 Fork ,欢迎各位下载使用并提出宝贵意见!
开源地址:
https://gitee.com/isrc_ohos/crop_image_layout_ohos
crop_image_layout_ohos 组件能对图片进行旋转和自定义裁切的操作,并且无论待裁切图片原尺寸有多大或多小,最终都将在以最佳尺寸在组件内显示。
同时,该组件操作界面简洁且使用方法简单,易被开发者使用或优化,能够提升应用的丰富性和可操作性。
组件效果展示
组件中可以通过操作图片、裁切框、按钮,最终实现在图片中裁切部分区域并进行显示的效果,组件的运行效果如图 1 所示。图 1:crop_image_layout_ohos 组件的运行效果图
对应运行效果图,详细解释其主要提供的功能:
点击“rotate”按钮可以对图片进行旋转操作。
手指按住裁切框内任意处并拖动,可实现裁切框移动,裁切框停止移动时,框内的图片即为想要裁切的图片。
被选中区域的左上角和右下角坐标会在图片下方的文本框中进行显示;
点击“crop”按钮可对选中的图片区域进行裁切,之后会跳转到第二个界面显示裁切后的图片。
Sample 解析
①组件的整体使用流程
如下图:
图 2:组件使用流程示意图
在介绍组件的使用前,先来介绍下构成 crop_image_layout_ohos 组件功能的 3 个重要部分:
裁切框:负责划定图片的裁切区域。
裁切图片:是指被导入组件中,即将被裁切的图片。
组件区域:是指组件所在的位置。
在这过程中,组件还实现了对裁切图片和裁切框尺寸的适配显示效果,这部分的具体原理会在 Library 解析部分进行讲解。 ②组件的具体使用步骤
下面介绍 crop_image_layout_ohos 组件的具体使用方法,共分为 6 个步骤:
步骤 1:在 xml 文件中添加 EditPhotoView 控件。
步骤 2:导入所需类并实例化类对象。
步骤 3:将裁切框坐标数据设置到裁切图片。
步骤 4:将裁切图片和裁切框添加到布局中
步骤 5:显示裁切框左上角和右下角坐标值。
步骤 6:设置监听事件。
在 xml 文件中以 com.huawei.croplayout.EditPhotoView 为控件名添加 EditPhotoView 控件,用来显示 crop_image_layout_ohos 的组件区域。
并分别设置裁切框拐角和边的颜色以及裁切图片未被选中部分的阴影颜色,如图所示。
图 3:属性设置示意图
//添加组件区域
ohos:id="$+id:editable_image"
...
crop:crop_corner_color="#45B4CA"//裁切框拐角颜色
crop:crop_line_color="#d7af55" //裁切框边颜色
crop:crop_shadow_color="#77ffffff"/> //裁切图片未被选中部分的阴影颜色
(2)导入所需类并实例化类对象
在 MainAbilitySlice 类的 onStart() 方法中,分别导入类:
onBoxChangedListener 类用于监听裁切框变化
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 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 和旋转图片等图片处理操作。 ①设置裁切框(实例化其尺寸)
如下图:
图 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;
}
②将裁切框坐标数据设置到裁切图片
获取裁切框列表 boxes 的第一个对象,即我们之前设置好对角线坐标的裁切框数据,并将其添加到裁切图片中。这是通过 EditableImage 类的 setBoxes() 方法实现的。
在该方法中,若裁切框对象列表 boxes 不为空且尺寸大于 0,则将 boxes 赋给 EditableImage 类的成员变量 originalBoxes,用于存储所有的裁剪框对象的数据。
将另一个此类的成员变量 copyofActiveBox 实例化,用于存储被选中的裁剪框两个角的坐标值。
其中,activeBoxIdx 是指在 boxes 中盛纳裁切框坐标的下标,List 可以为用户预留多个裁切框坐标,本组件中只是用下表为 0 的裁切框坐标。
public void setBoxes(List 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());
}
}
③将裁切图片和裁剪框添加到布局中
该功能是通过 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 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);
全部0条评论
快来发表一下你的评论吧 !