鴻蒙開(kāi)源第三方組件——crop_image_layout_ohos
想了解更多內(nèi)容,請(qǐng)?jiān)L問(wèn):
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
前言
基于安卓平臺(tái)的圖片裁切組件crop_image_layout(https://github.com/yulu/crop-image-layout ),實(shí)現(xiàn)了鴻蒙化遷移和重構(gòu),代碼已經(jīng)開(kāi)源到(https://gitee.com/isrc_ohos/crop_image_layout_ohos ),目前已經(jīng)獲得了很多人的Star和Fork ,歡迎各位下載使用并提出寶貴意見(jiàn)!
背景
crop_image_layout_ohos組件能對(duì)圖片進(jìn)行旋轉(zhuǎn)和自定義裁切的操作,并且無(wú)論待裁切圖片原尺寸有多大或多小,最終都將在以最佳尺寸在組件內(nèi)顯示。同時(shí),該組件操作界面簡(jiǎn)潔且使用方法簡(jiǎn)單,易被開(kāi)發(fā)者使用或優(yōu)化,能夠提升應(yīng)用的豐富性和可操作性。
組件效果展示
組件中可以通過(guò)操作圖片、裁切框、按鈕,最終實(shí)現(xiàn)在圖片中裁切部分區(qū)域并進(jìn)行顯示的效果,組件的運(yùn)行效果如圖1所示。

圖1 crop_image_layout_ohos組件的運(yùn)行效果圖
對(duì)應(yīng)運(yùn)行效果圖,詳細(xì)解釋其主要提供的功能:
- 點(diǎn)擊“rotate”按鈕可以對(duì)圖片進(jìn)行旋轉(zhuǎn)操作。
- 手指按住裁切框內(nèi)任意處并拖動(dòng),可實(shí)現(xiàn)裁切框移動(dòng),裁切框停止移動(dòng)時(shí),框內(nèi)的圖片即為想要裁切的圖片;
- 被選中區(qū)域的左上角和右下角坐標(biāo)會(huì)在圖片下方的文本框中進(jìn)行顯示;
- 點(diǎn)擊“crop”按鈕可對(duì)選中的圖片區(qū)域進(jìn)行裁切,之后會(huì)跳轉(zhuǎn)到第二個(gè)界面顯示裁切后的圖片;
Sample解析
1.組件的整體使用流程
圖2 組件使用流程示意圖
在介紹組件的使用前,先來(lái)介紹下構(gòu)成crop_image_layout_ohos組件功能的3個(gè)重要部分:
-裁切框:負(fù)責(zé)劃定圖片的裁切區(qū)域;
-裁切圖片:是指被導(dǎo)入組件中,即將被裁切的圖片;
-組件區(qū)域:是指組件所在的位置;
組件的使用過(guò)程可以概括為首先設(shè)置裁切框的位置(坐標(biāo)),然后將裁切框坐標(biāo)數(shù)據(jù)添加到裁切圖片中,最后將裁切框和裁切圖片添加到組件區(qū)域中。
在這過(guò)程中,組件還實(shí)現(xiàn)了對(duì)裁切圖片和裁切框尺寸的適配顯示效果,這部分的具體原理會(huì)在Library解析部分進(jìn)行講解。
2.組件的具體使用步驟
下面介紹crop_image_layout_ohos組件的具體使用方法,共分為6個(gè)步驟:
步驟1. 在xml文件中添加EditPhotoView控件。
步驟2. 導(dǎo)入所需類(lèi)并實(shí)例化類(lèi)對(duì)象。
步驟3. 將裁切框坐標(biāo)數(shù)據(jù)設(shè)置到裁切圖片。
步驟4. 將裁切圖片和裁切框添加到布局中
步驟5. 顯示裁切框左上角和右下角坐標(biāo)值。
步驟6. 設(shè)置監(jiān)聽(tīng)事件。
(1)在xml文件中添加EditPhotoView控件
在xml文件中以com.huawei.croplayout.EditPhotoView為控件名添加EditPhotoView控件,用來(lái)顯示crop_image_layout_ohos的組件區(qū)域。并分別設(shè)置裁切框拐角和邊的顏色以及裁切圖片未被選中部分的陰影顏色,如圖所示。
圖3 屬性設(shè)置示意圖
- <com.huawei.croplayout.EditPhotoView//添加組件區(qū)域
- ohos:id="$+id:editable_image"
- ...
- crop:crop_corner_color="#45B4CA"//裁切框拐角顏色
- crop:crop_line_color="#d7af55" //裁切框邊顏色
- crop:crop_shadow_color="#77ffffff"/> //裁切圖片未被選中部分的陰影顏色
(2)導(dǎo)入所需類(lèi)并實(shí)例化類(lèi)對(duì)象
在MainAbilitySlice類(lèi)的onStart()方法中,分別導(dǎo)入類(lèi)onBoxChangedListeneron、EditPhotoView、EdittableImage、ScalableBox。
- BoxChangedListener類(lèi)用于監(jiān)聽(tīng)裁切框變化;
- EditPhotoView類(lèi)用于設(shè)置組件區(qū)域;
- EdittableImage類(lèi)用于設(shè)置裁切圖片;
- ScalableBox類(lèi)用于設(shè)置裁切框;
- import com.example.croplayout.handler.OnBoxChangedListener;//裁切框變化監(jiān)聽(tīng)
- import com.example.croplayout.EditPhotoView;//組件區(qū)域
- import com.example.croplayout.EditableImage;//裁切圖片
- import com.example.croplayout.model.ScalableBox;//裁切框
創(chuàng)建EditPhotoView類(lèi)和Text類(lèi)對(duì)象分別用于綁定crop_image_layout_ohos的組件區(qū)域和用于顯示裁切框坐標(biāo)的文本控件;實(shí)例化EdittableImage類(lèi)對(duì)象image,使其包含圖1所示的裁切圖片,實(shí)現(xiàn)組件裁切圖片的導(dǎo)入。創(chuàng)建一個(gè)元素類(lèi)型為ScalableBox的List,并將其命名為boxes,用于盛納裁切框的坐標(biāo)。新實(shí)例化一個(gè)左上角坐標(biāo)為(25,180)、右下角坐標(biāo)為(640,880)的裁切框?qū)ο?,并調(diào)用add()方法將其添加到上述boxes中。
- //用于綁定組件的裁切圖片視圖區(qū)域
- 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<>();//用于設(shè)置裁切框的坐標(biāo)
- boxes.add(new ScalableBox(25, 180, 640, 880));//裁切框的坐標(biāo)
(3)將裁切框坐標(biāo)數(shù)據(jù)設(shè)置到裁切圖片
通過(guò)setBoxes()方法將boxes中裁切框?qū)ο蟮淖鴺?biāo)數(shù)據(jù)設(shè)置到裁切圖片image中,實(shí)現(xiàn)裁切框相對(duì)裁切圖片的位置設(shè)定;
- image.setBoxes(boxes);
(4)將裁切圖片和裁切框添加到布局中
調(diào)用intView()方法,創(chuàng)建裁切圖片和裁切框的視圖,并將其添加到組件布局中進(jìn)行顯示。
- imageView.initView(this, image);
(5)顯示裁切框左上角和右下角坐標(biāo)值
重新聲明一個(gè)ScalableBox類(lèi)型的對(duì)象activeBox,用于動(dòng)態(tài)取裁切框的坐標(biāo),并將其通過(guò)Text在界面上顯示出來(lái)。
- ScalableBox activeBox = image.getActiveBox();//動(dòng)態(tài)獲取圖片中裁切框選取區(qū)域的坐標(biāo)
- boxText.setText("box: [" + activeBox.getX1() + "," + activeBox.getY1() +
- "],[" + activeBox.getX2() + "," + activeBox.getY2() + "]");
(6)設(shè)置監(jiān)聽(tīng)事件
組件區(qū)域監(jiān)聽(tīng)事件
為組件區(qū)域?qū)ο骾mageView設(shè)置監(jiān)聽(tīng)事件,當(dāng)裁切框位置發(fā)生變化時(shí),將其坐標(biāo)設(shè)置到Text對(duì)象boxText中進(jìn)行顯示。
- imageView.setOnBoxChangedListener(new OnBoxChangedListener() {
- @Override//設(shè)置裁切框區(qū)域監(jiān)聽(tīng)事件
- public void onChanged(int x1, int y1, int x2, int y2) {
- boxText.setText("box: [" + x1 + "," + y1 + "],[" + x2 + "," + y2 + "]");
- }
- });
旋轉(zhuǎn)按鈕“rotate”監(jiān)聽(tīng)事件
聲明一個(gè)Button類(lèi)對(duì)象rotateButton將其與“rotate_button”控件綁定。為其設(shè)置點(diǎn)擊監(jiān)聽(tīng)事件,按鈕被點(diǎn)擊時(shí),通過(guò)組件區(qū)域?qū)ο骾mageView調(diào)用rotateImageView()方法實(shí)現(xiàn)裁切圖片向右旋轉(zhuǎn)90°的效果。
- Button rotateButton = (Button) findComponentById(ResourceTable.Id_rotate_button);//與”rorate_button“控件綁定
- rotateButton.setClickedListener(new Component.ClickedListener() {
- @Override//設(shè)置點(diǎn)擊監(jiān)聽(tīng)事件
- public void onClick(Component component) {
- imageView.rotateImageView();//實(shí)現(xiàn)裁切圖片向右旋轉(zhuǎn)90°
- }
- });
裁切按鈕“crop”監(jiān)聽(tīng)事件
聲明一個(gè)Button類(lèi)對(duì)象cropButton將其與“crop_button”控件綁定,并設(shè)置點(diǎn)擊監(jiān)聽(tīng)事件。按鈕被點(diǎn)擊時(shí),通過(guò)裁切圖片image調(diào)用cropOriginalImage()方法得到裁切后的圖片并將其存放于PixelMap類(lèi)對(duì)象中;通過(guò)Intent跳轉(zhuǎn)到第二個(gè)界面,并將裁切后的圖片作為參數(shù)傳入,顯示在第二個(gè)界面中。
- Button cropButton = (Button) findComponentById(ResourceTable.Id_crop_button);
- cropButton.setClickedListener(new Component.ClickedListener() {
- @Override//設(shè)置點(diǎn)擊監(jiān)聽(tīng)事件
- public void onClick(Component component) {
- PixelMap croppedImage = image.cropOriginalImage();
- Intent newIntent = new Intent();
- newIntent.setParam("image", croppedImage);
- present(new SecondAbilitySlice(), newIntent);
- }
- });
Library解析
- Library部分將圍繞圖2,對(duì)crop_image_layout_ohos組件的原理和執(zhí)行邏輯進(jìn)行梳理。其中會(huì)涉及到ScalableBox類(lèi)、EdittableImage類(lèi)、EditPhotoView類(lèi)、SelectionView類(lèi)、和ImageHelper類(lèi);
- ScalableBox類(lèi)、EdittableImage類(lèi)、EditPhotoView類(lèi)在上一節(jié)中簡(jiǎn)單介紹過(guò)是用來(lái)設(shè)置組件區(qū)域、裁切圖片和裁切框的;
- SelectionView類(lèi)用于用于設(shè)置裁剪框所在的視圖;
- ImageHelper類(lèi)相當(dāng)于一個(gè)圖片操作輔助工具,用來(lái)完成從原裁剪圖片中獲取PixelMap和旋轉(zhuǎn)圖片等圖片處理操作。
1.設(shè)置裁切框(實(shí)例化其尺寸)

圖4 裁切框坐標(biāo)示意圖
在Sample解析中我們講過(guò),需要調(diào)用add()方法將新實(shí)例化的左上角坐標(biāo)為(25,180)、右下角坐標(biāo)為(640,880)的裁切框加入到boxes對(duì)象中。其中,左上角坐標(biāo)對(duì)應(yīng)圖4中的(X1,Y1),右上角對(duì)應(yīng)圖4中的(X2,Y2),通過(guò)設(shè)置裁切框?qū)蔷€上兩個(gè)點(diǎn),就可以唯一確定其大小和位置了。
- boxes.add(new ScalableBox(25, 180, 640, 880));
實(shí)例化過(guò)程需要通過(guò)ScalableBox類(lèi)的構(gòu)造函數(shù),設(shè)置裁切框左上角和右下角的坐標(biāo)。
- public ScalableBox(int x1, int y1, int x2, int y2) {
- this.x1 = x1;
- this.y1 = y1;
- this.x2 = x2;
- this.y2 = y2;
- }
2.將裁切框坐標(biāo)數(shù)據(jù)設(shè)置到裁切圖片
獲取裁切框列表boxes的第一個(gè)對(duì)象,即我們之前設(shè)置好對(duì)角線坐標(biāo)的裁切框數(shù)據(jù),并將其添加到裁切圖片中。這是通過(guò)EditableImage類(lèi)的setBoxes()方法實(shí)現(xiàn)的。
在該方法中,若裁切框?qū)ο罅斜韇oxes不為空且尺寸大于0,則將boxes賦給EditableImage類(lèi)的成員變量originalBoxes,用于存儲(chǔ)所有的裁剪框?qū)ο蟮臄?shù)據(jù);將另一個(gè)此類(lèi)的成員變量copyofActiveBox實(shí)例化,用于存儲(chǔ)被選中的裁剪框兩個(gè)角的坐標(biāo)值;其中,activeBoxIdx是指在boxes中盛納裁切框坐標(biāo)的下標(biāo),List可以為用戶預(yù)留多個(gè)裁切框坐標(biāo),本組件中只是用下表為0的裁切框坐標(biāo)。
- public void setBoxes(List<ScalableBox> boxes, int activeBoxIdx) {
- if (boxes != null && boxes.size() > 0) {//如果boxes對(duì)象不為空且尺寸大于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.將裁切圖片和裁剪框添加到布局中
該功能是通過(guò)EditPhotoView類(lèi)的initView()方法實(shí)現(xiàn)的。先根據(jù)創(chuàng)建EditPhotoView對(duì)象時(shí)傳入的裁切框尺寸、邊角尺寸和顏色等屬性,將SelectionView類(lèi)實(shí)例化,用于展示裁剪框的視圖;然后根據(jù)傳入的裁切圖片實(shí)例化得到Image圖片類(lèi)對(duì)象,用于展示裁切圖片的視圖;再將SelectionView類(lèi)對(duì)象和Image對(duì)象的布局設(shè)置為跟隨父組件,并將兩者添加到裁切組件區(qū)域布局中,實(shí)現(xiàn)裁切框和裁切圖片的顯示。
分別通過(guò)setViewSize()方法設(shè)置裁切圖片視圖區(qū)域尺寸、setPixelMap()為其設(shè)置裁切圖片位圖格式、setScaleMode()方法為其設(shè)置圖片縮放模式為中心縮放、setBoxSize()方法設(shè)置裁切圖片和裁切框適配后的尺寸。
- 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);//設(shè)置選擇區(qū)域尺寸、邊角尺寸以及顏色
- imageView.setLayoutConfig(new LayoutConfig(LayoutConfig.MATCH_PARENT, LayoutConfig.MATCH_PARENT));//跟隨父組件
- selectionView.setLayoutConfig(new LayoutConfig(LayoutConfig.MATCH_PARENT, LayoutConfig.MATCH_PARENT));
- addComponent(imageView, 0);//將裁切框區(qū)域和選擇區(qū)域添加到布局中
- 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)在裁切圖片視圖區(qū)域中適配裁切圖片
由于裁切圖片的視圖區(qū)域與組件區(qū)域的大小相同,二裁切圖片的大小是不固定的,因此裁切圖片在顯示到其視圖區(qū)域時(shí),需進(jìn)行尺寸的適配。
上述功能是由EditableImage類(lèi)的getFitSize()方法提供,該方法在上述setBoxSize()方法中被調(diào)用。通過(guò)該方法能夠?qū)⒉们袌D片與其視圖區(qū)域進(jìn)行適配,同時(shí)返回適配后圖片尺寸。這是為了更好地在裁切圖片視圖區(qū)域中展示圖片,無(wú)論過(guò)大或過(guò)小尺寸的圖片都能在此區(qū)域中被縮放至最合適的程度顯示。原理可參考圖5。
圖5 適配圖片尺寸原理圖(左:ratio>viewRatio,右反之)
先計(jì)算原裁切圖片(即粉色矩形)寬和高的比值ratio(即a/b)和組件區(qū)域(即黃色矩形)寬和高的比值viewRatio(即c/d)。
判斷若原裁切圖片寬高比大于裁切圖片視圖區(qū)域?qū)捀弑燃磮D5中左圖的情況,則說(shuō)明可以將原裁切圖片最大程度放大至寬a與裁切圖片視圖區(qū)域?qū)抍長(zhǎng)度一致的尺寸(即藍(lán)色矩形),此時(shí)原裁切圖片高b按ratio放大后的長(zhǎng)度一定小于裁切圖片視圖區(qū)域高d,因此可以根據(jù)圖片寬放大的倍數(shù)factor求出放大后高的長(zhǎng)度。
若原裁切圖片寬高比小于裁切圖片視圖區(qū)域?qū)捀弑燃磮D5中右圖的情況,與上一種情況同理,則說(shuō)明可以將原裁切圖片最大程度放大至高b與裁切圖片視圖區(qū)域高d長(zhǎng)度一致的尺寸(即藍(lán)色矩形),此時(shí)原裁切圖片寬a按ratio放大后的長(zhǎng)度一定小于裁切圖片視圖區(qū)域?qū)抍,因此可以根據(jù)圖片高放大的倍數(shù)factor求出放大后寬的長(zhǎng)度。
計(jì)算完成后將圖片放大后的寬和高分別存放在int型數(shù)組fitSize[]中。上述是以原裁切圖片尺寸小于裁切圖片視圖區(qū)域?yàn)槔粗怼?/p>
- public int[] getFitSize() {//適配圖片,將圖片縮放至比例與裁切圖片視圖區(qū)域比例一致
- int[] fitSize = new int[2];//用于存放適配后的圖片寬高
- //原裁剪圖片寬高比
- float ratio = originalPixelMap.getImageInfo().size.width / (float) originalPixelMap.getImageInfo().size.height;
- float viewRatio = viewWidth / (float) viewHeight;//裁切圖片視圖區(qū)域?qū)捀弑?nbsp;
- //原裁剪圖片寬和高比例大于裁切圖片視圖區(qū)域?qū)捄透弑壤?nbsp;
- if (ratio > viewRatio) {
- float factor = viewWidth / (float) originalPixelMap.getImageInfo().size.width;//裁切圖片寬放大的倍數(shù)
- fitSize[0] = viewWidth;//寬為裁切圖片視圖區(qū)域?qū)?nbsp;
- fitSize[1] = (int) (originalPixelMap.getImageInfo().size.height * factor);//根據(jù)寬放大的倍數(shù)計(jì)算放大后高的長(zhǎng)度
- } else { //原裁剪圖片寬和高比例小于裁切圖片視圖區(qū)域?qū)捄透弑壤?nbsp;
- float factor = viewHeight / (float) originalPixelMap.getImageInfo().size.height;
- fitSize[0] = (int) (originalPixelMap.getImageInfo().size.width * factor);
- fitSize[1] = viewHeight;
- }
- return fitSize;
- }
(2)將裁切框和圖片進(jìn)行適配
裁切框需要與裁切圖片保持相同的顯示比例,因此裁切框需要和裁切圖片進(jìn)行適配。
上述功能是由SelectionView類(lèi)的setBoxsize()方法。獲取適配后圖片的寬高,與裁切框?qū)捀哌M(jìn)行計(jì)算得到originX和originY,并調(diào)用setDisplayBoxes()方法設(shè)置適配后裁切框的坐標(biāo)。
- public void setBoxSize(EditableImage editableImage, List<ScalableBox> originalBoxes, int widthX, int heightY) {
- int[] fitSize = editableImage.getFitSize();//獲取前面計(jì)算地適配后的圖片尺寸
- 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);//設(shè)置適配后裁切框的坐標(biāo)
- invalidate();
- }
setDisplayBoxes()方法中核心部分是根據(jù)圖片縮放比例計(jì)算適配后裁切框?qū)蔷€上兩點(diǎn)的坐標(biāo)。先計(jì)算圖片縮放前后寬的比值scale(即c/a),用之前實(shí)例化時(shí)設(shè)置的裁切框初始尺寸的左上角橫坐標(biāo)X1與縮放比例scale相乘得到適配后的橫坐標(biāo),再加上前面計(jì)算好的originX,即得到適配后的裁切框左上角橫坐標(biāo)scaleX1,右下角橫坐標(biāo)scaleX2、左上角豎坐標(biāo)scaleY1、右下角豎坐標(biāo)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);
想了解更多內(nèi)容,請(qǐng)?jiān)L問(wèn):
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)