鴻蒙開源第三方組件—uCrop_ohos圖片裁剪組件
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
前言
基于安卓平臺的圖片裁剪組件uCrop( https://github.com/Yalantis/uCrop),實現(xiàn)了鴻蒙化遷移和重構(gòu)。目前代碼已經(jīng)開源到(https://gitee.com/isrc_ohos/u-crop_ohos),歡迎各位下載使用并提出寶貴意見!
背景
uCrop組件是開源的圖片裁剪庫,支持對圖片的縮放和裁剪等操作,是安卓平臺比較受歡迎的組件,在Github上已有1萬多個Star和近2千個Fork。uCrop組件具有封裝程度高、使用流暢、自定義程度高的優(yōu)點,被廣泛應(yīng)用于多種APP中。
組件效果展示
安卓和鴻蒙UI組件的差異較大,uCrop_ohos的實現(xiàn)完全重構(gòu)了安卓版uCrop的UI部分,所以uCrop_ohos的組件效果看上去會和uCrop完全不同。
本組件的效果展示可分為兩個步驟:圖片選擇和圖片裁剪。下面依次對其進行講解和展示。
1、uCrop_ohos圖片選擇
uCrop_ohos支持裁剪系統(tǒng)選擇相冊圖片或網(wǎng)絡(luò)圖片,用戶可以在主菜單中選擇對應(yīng)的功能,如圖1所示。
圖 1 主菜單界面
(1)uCrop_ohos讀取相冊圖片
當(dāng)用戶賦予組件相應(yīng)權(quán)限后,uCrop_ohos可以自動讀取手機相冊中每一張圖片,并將它們的縮略圖作為一個列表呈現(xiàn)在UI界面上,用戶可以上下滑動列表尋找目標(biāo)圖片,如圖2所示。當(dāng)用戶點擊某張縮略圖時,會跳轉(zhuǎn)到uCrop_ohos的裁剪界面,執(zhí)行后續(xù)操作。
圖 2 選擇系統(tǒng)相冊圖片
(2)uCrop_ohos讀取網(wǎng)絡(luò)圖片
用戶需要將圖片網(wǎng)址鍵入到輸入框內(nèi)并點擊確定按鈕,如圖3所示。uCrop_ohos會自動下載圖片并跳轉(zhuǎn)到裁剪界面,執(zhí)行后續(xù)操作。
圖 3 選擇網(wǎng)絡(luò)圖片
2、uCrop_ohos圖片裁剪
圖4 uCrop_ohos的裁剪界面
圖4是uCrop_ohos的裁剪界面。使用者可以通過手勢對圖片進行縮放、旋轉(zhuǎn)和平移的操作,也可以通過按鈕、滑塊等控件進行相應(yīng)操作。將圖片調(diào)整至滿意狀態(tài)時,點擊裁剪按鈕即可獲得裁剪后的新圖片,并將其保存至手機相冊。且本組件的圖片與裁剪框具有自適應(yīng)能力,能夠保證裁剪框時刻在圖片范圍內(nèi),防止由于裁剪框的范圍大于圖片導(dǎo)致的一系列問題。
Sample解析
圖5 Sample的工程結(jié)構(gòu)
uCrop_ohos的核心能力都由其Library提供,Sample主要用于構(gòu)建UI,并調(diào)用Library的接口。從圖5可以看出Sample的工程結(jié)構(gòu)較為簡單,主要由4個文件構(gòu)成,下面進行詳細的介紹。
1、CropPicture
CropPicture文件提供了裁剪界面,其最主要的邏輯是通過圖片Uri實例化Library中UCropView類。由于uCrop_ohos的邏輯是先將用戶選擇的原圖創(chuàng)建一個副本,然后對副本執(zhí)行裁剪,所以為了將圖片傳入UCropView需要兩個Uri:一個名為uri_i,從intent中獲得,標(biāo)識的是用戶選擇的原圖,可以是本地圖片也可以是網(wǎng)絡(luò)圖片;另一個名為uri_o,標(biā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給開發(fā)者提供了public接口,使得開發(fā)者易于封裝自己的UI功能。例如本文件中的旋轉(zhuǎn)和縮放滑塊、旋轉(zhuǎn)和縮放按鈕、當(dāng)前旋轉(zhuǎn)和縮放狀態(tài)的顯示都是調(diào)用Library接口實現(xiàn)的。以如下功能的實現(xiàn)為例:創(chuàng)建了一個按鈕,當(dāng)用戶觸碰這個按鈕之后就可以將圖片右旋90度。其核心能力就是依靠調(diào)用Library中postRotate()函數(shù)實現(xiàn)的,非常簡單。
- //右旋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;
- //計算旋轉(zhuǎn)中心
- float center_X = uCropView.getOverlayView().getCropViewRect().getCenter().getPointX();
- float center_Y = uCropView.getOverlayView().getCropViewRect().getCenter().getPointY();
- //旋轉(zhuǎn)
- uCropView.getCropImageView().postRotate(degrees,center_X,center_Y);
- //適配
- uCropView.getCropImageView().setImageToWrapCropBounds(false);
- //顯示旋轉(zhuǎn)角度
- mDegree = uCropView.getCropImageView().getCurrentAngle();
- text.setText("當(dāng)前旋轉(zhuǎn)角度: " + df.format(mDegree) + " °");
- }
- });
2、LocalPictureChoose & HttpPictureChoose
由上文可知,uri_i是通過intent得到的,這個intent就是由 LocalPictureChoose或HttpPictureChoose傳遞的。LocalPictureChoose提供選擇相冊圖片的能力,HttpPictureChoose提供選擇網(wǎng)絡(luò)圖片的能力。
LocalPictureChoose提供的功能是將相冊中的全部圖片讀取出來,做成縮略圖排列在UI上,然后將每個縮略圖綁定一個觸摸監(jiān)聽器,一旦使用者選中某個縮略圖,就會將這個縮略圖對應(yīng)的原圖uri放在intent中傳給CropPicture。具體代碼如下:
- private void showImage() {
- DataAbilityHelper helper = DataAbilityHelper.creator(this);
- try {
- // columns為null,查詢記錄所有字段,當(dāng)前例子表示查詢id字段
- ResultSet resultSet = helper.query(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI, new String[]{AVStorage.Images.Media.ID}, null);
- while (resultSet != null && resultSet.goToNextRow()) {
- //創(chuàng)建image用以顯示系統(tǒng)相冊縮略圖
- PixelMap pixelMap = null;
- ImageSource imageSource = null;
- Image image = new Image(this);
- image.setWidth(250);
- image.setHeight(250);
- image.setMarginsLeftAndRight(10, 10);
- image.setMarginsTopAndBottom(10, 10);
- 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的功能主要是將用戶輸入的網(wǎng)絡(luò)圖片地址解析為Uri傳遞給CropPicture,目前只支持手動輸入地址。
3、MainMenu
一個簡單的主菜單界面,用戶可以通過點擊不同的按鈕選擇裁剪相冊圖片還是網(wǎng)絡(luò)圖片。
Library解析
鴻蒙和安卓存在較多的能力差異,即二者在實現(xiàn)同一 種功能時,方法不同,這不僅體現(xiàn)在工程結(jié)構(gòu)上,也體現(xiàn)在具體的代碼邏輯中。以下將對uCrop_ohos和uCrop的工程結(jié)構(gòu)進行對比,并介紹幾個在uCrop_ohos移植過程中遇到的安卓和鴻蒙的能力差異。
1、工程結(jié)構(gòu)對比
圖 6 uCrop_ohos(上)與uCrop(下)的工程結(jié)構(gòu)對比
可以看出uCrop_ohos相比uCrop少封裝了一層Activity與Fragment,原因有3個:
(1)安卓的Activity與鴻蒙的Ability還是有差別的,強行復(fù)現(xiàn)會導(dǎo)致代碼復(fù)用率低。
(2)這一層與UI強耦合,由于鴻蒙尚不支持安卓中許多控件,例如Menu等,這就導(dǎo)致難以原樣復(fù)現(xiàn)UCropActivity中的UI。
(3)封裝程度越高,可供開發(fā)者自定義的程度就越小。
2、能力差異
(1)圖片加載&保存
不論是加載網(wǎng)絡(luò)圖片還是相冊圖片,在uCrop和uCrop_ohos內(nèi)部都是通過解析圖片的Uri實現(xiàn)的,所以需要有一個識別Uri種類的過程,即通過分析Uri的Scheme來實現(xiàn)Uri的分類。如果Uri的Scheme是http或https則會被認為是網(wǎng)絡(luò)圖片,調(diào)用okhttp3的能力執(zhí)行下載操作;如果Uri的Scheme是content(安卓)或dataability(鴻蒙)就會被認為是本地圖片,執(zhí)行復(fù)制操作。下載或復(fù)制的圖片將作為被裁剪的圖片。代碼如下所示:
- private void processInputUri() throws NullPointerException, IOException {
- String inputUriScheme = mInputUri.getScheme();
- //Scheme為http或https即為網(wǎng)絡(luò)圖片,執(zhí)行下載
- if ("http".equals(inputUriScheme) || "https".equals(inputUriScheme)) {
- try {
- downloadFile(mInputUri, mOutputUri);
- } catch (NullPointerException e) {
- LogUtils.LogError(TAG, "Downloading failed:"+e);
- throw e;
- }
- //安卓中Scheme為content即為本地圖片,執(zhí)行復(fù)制
- } else if ("content".equals(inputUriScheme)) {
- try {
- copyFile(mInputUri, mOutputUri);
- } catch (NullPointerException | IOException e) {
- LogUtils.LogError(TAG, "Copying failed:"+e);
- throw e;
- }
- //鴻蒙中Scheme為dataability即為本地圖片,執(zhí)行復(fù)制
- } else if("dataability".equals(inputUriScheme)){
- try {
- copyFile(mInputUri, mOutputUri);
- } catch (NullPointerException | IOException e) {
- LogUtils.LogError(TAG, "Copying failed:"+e);
- throw e;
- }
圖片文件準(zhǔn)備完成后,還需要將其解碼成Bitmap(安卓)或PixelMap(鴻蒙)格式以便實現(xiàn)uCrop后續(xù)的各種功能。在解碼之前還需要通過Uri來獲取文件流,在這一點上安卓和鴻蒙的實現(xiàn)原理不同。對于安卓,可以通過openInputStream()函數(shù)獲得輸入文件流InputStream:
- InputStream stream = mContext.getContentResolver().openInputStream(mInputUri);
對于鴻蒙則需要調(diào)用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"寫權(quán)限
- FileDescriptor fd = helper.openFile(uri, "w");
- OutputStream outputStream = new FileOutputStream(fd);
(2)裁剪的實現(xiàn)
在安卓版的uCrop中,裁剪功能的實現(xiàn)原理是將原圖(位圖1)位于裁剪框內(nèi)的部分創(chuàng)建一個新的位圖(位圖2),然后將新的位圖保存成圖片文件(圖片文件1)。如圖7所示:
圖 7 uCrop裁剪功能的實現(xiàn)方法
而在鴻蒙版uCrop_ohos中,裁剪功能的實現(xiàn)原理發(fā)生了變化。鴻蒙系統(tǒng)API雖不支持對位圖的旋轉(zhuǎn)操作,但圖像的解碼API提供了旋轉(zhuǎn)能力,所以鴻蒙的裁剪過程是這樣的:
首先將原圖(位圖1)保存為一個臨時的圖片文件(圖片文件1),通過相對旋轉(zhuǎn)角度對臨時圖片文件進行讀取,此時讀取出的位圖(位圖2)就包含了正確的旋轉(zhuǎn)信息。然后再通過相對縮放和位移創(chuàng)建一個新的位圖(位圖3),這個位圖還會因為API的特性發(fā)生壓縮和錯切等形變,所以還需要再創(chuàng)建最后一個位圖(位圖4)來修正形變,最后再將位圖4保存成圖片文件(圖片文件2)。如圖8所示:
圖 8 uCrop_ohos裁剪功能的實現(xiàn)方法
(3)異步任務(wù)處理
由于圖片的讀取、裁剪和保存這些操作都是比較消耗系統(tǒng)性能的,直接導(dǎo)致的問題就是卡頓,所以需要使用異步任務(wù)將這些操作放到后臺操作,減少UI線程的負擔(dān)。下面以裁剪任務(wù)為例進行介紹。
在uCrop中使用的是BitmapCropTask類繼承AsyncTask類的方法:
- public class BitmapCropTask extends AsyncTask<Void, Void, Throwable>
然后在其中重寫doInBackground()和onPostExecute()函數(shù),分別實現(xiàn)后臺裁剪任務(wù)的處理與回調(diào):
- @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修改了后臺任務(wù)的處理方案,首先將后臺任務(wù)的處理與回調(diào)合并寫在一個Runnable中,然后鴻蒙原生的多線程處理機制EventHandler搭配EventRunner新開一個線程用于處理這個Runnable,實現(xiàn)了圖片裁剪任務(wù)的異步處理。
- 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();
- }
- }
- });
- }
項目貢獻人
吳圣垚 鄭森文 朱偉 陳美汝 王佳思
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)