鴻蒙開源第三方組件——uCrop_ohos圖片裁剪組件

目錄:git

一、前言github

二、背景網絡

三、組件效果展現多線程

四、Sample解析app

五、Library解析異步

六、《鴻蒙開源第三方組件》系列文章合集ide

前言函數

       基於安卓平臺的圖片裁剪組件uCrop( https://github.com/Yalantis/uCrop),實現了鴻蒙化遷移和重構。目前代碼已經開源到(https://gitee.com/isrc_ohos/u-crop_ohos),歡迎各位下載使用並提出寶貴意見!post

  背景性能

       uCrop組件是開源的圖片裁剪庫,支持對圖片的縮放和裁剪等操做,是安卓平臺比較受歡迎的組件,在Github上已有1萬多個Star和近2千個Fork。uCrop組件具備封裝程度高、使用流暢、自定義程度高的優勢,被普遍應用於多種APP中。

組件效果展現

      安卓和鴻蒙UI組件的差別較大,uCrop_ohos的實現徹底重構了安卓版uCrop的UI部分,因此uCrop_ohos的組件效果看上去會和uCrop徹底不一樣。

      本組件的效果展現可分爲兩個步驟:圖片選擇和圖片裁剪。下面依次對其進行講解和展現。

一、uCrop_ohos圖片選擇

      uCrop_ohos支持裁剪系統選擇相冊圖片或網絡圖片,用戶能夠在主菜單中選擇對應的功能,如圖1所示。

鴻蒙開源第三方組件——uCrop_ohos圖片裁剪組件

圖 1 主菜單界面

      (1)uCrop_ohos讀取相冊圖片

       當用戶賦予組件相應權限後,uCrop_ohos能夠自動讀取手機相冊中每一張圖片,並將它們的縮略圖做爲一個列表呈如今UI界面上,用戶能夠上下滑動列表尋找目標圖片,如圖2所示。當用戶點擊某張縮略圖時,會跳轉到uCrop_ohos的裁剪界面,執行後續操做。

鴻蒙開源第三方組件——uCrop_ohos圖片裁剪組件

圖 2 選擇系統相冊圖片

(2)uCrop_ohos讀取網絡圖片

       用戶須要將圖片網址鍵入到輸入框內並點擊肯定按鈕,如圖3所示。uCrop_ohos會自動下載圖片並跳轉到裁剪界面,執行後續操做。

鴻蒙開源第三方組件——uCrop_ohos圖片裁剪組件

圖 3 選擇網絡圖片

二、uCrop_ohos圖片裁剪

鴻蒙開源第三方組件——uCrop_ohos圖片裁剪組件

圖4  uCrop_ohos的裁剪界面

        圖4是uCrop_ohos的裁剪界面。使用者能夠經過手勢對圖片進行縮放、旋轉和平移的操做,也能夠經過按鈕、滑塊等控件進行相應操做。將圖片調整至滿意狀態時,點擊裁剪按鈕便可得到裁剪後的新圖片,並將其保存至手機相冊。且本組件的圖片與裁剪框具備自適應能力,可以保證裁剪框時刻在圖片範圍內,防止因爲裁剪框的範圍大於圖片致使的一系列問題。

Sample解析

鴻蒙開源第三方組件——uCrop_ohos圖片裁剪組件

圖5 Sample的工程結構

        uCrop_ohos的核心能力都由其Library提供,Sample主要用於構建UI,並調用Library的接口。從圖5能夠看出Sample的工程結構較爲簡單,主要由4個文件構成,下面進行詳細的介紹。

一、CropPicture

        CropPicture文件提供了裁剪界面,其最主要的邏輯是經過圖片Uri實例化Library中UCropView類。因爲uCrop_ohos的邏輯是先將用戶選擇的原圖建立一個副本,而後對副本執行裁剪,因此爲了將圖片傳入UCropView須要兩個Uri:一個名爲uri_i,從intent中得到,標識的是用戶選擇的原圖,能夠是本地圖片也能夠是網絡圖片;另外一個名爲uri_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給開發者提供了public接口,使得開發者易於封裝本身的UI功能。例如本文件中的旋轉和縮放滑塊、旋轉和縮放按鈕、當前旋轉和縮放狀態的顯示都是調用Library接口實現的。以以下功能的實現爲例:建立了一個按鈕,當用戶觸碰這個按鈕以後就能夠將圖片右旋90度。其核心能力就是依靠調用Library中postRotate()函數實現的,很是簡單。

//右旋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;
        //計算旋轉中心
        float center_X = uCropView.getOverlayView().getCropViewRect().getCenter().getPointX();
        float center_Y = uCropView.getOverlayView().getCropViewRect().getCenter().getPointY();
        //旋轉
        uCropView.getCropImageView().postRotate(degrees,center_X,center_Y);
        //適配
        uCropView.getCropImageView().setImageToWrapCropBounds(false);
        //顯示旋轉角度
        mDegree = uCropView.getCropImageView().getCurrentAngle();
        text.setText("當前旋轉角度: " + df.format(mDegree) + " °");
    }
});

二、LocalPictureChoose & HttpPictureChoose

       由上文可知,uri_i是經過intent獲得的,這個intent就是由 LocalPictureChoose或HttpPictureChoose傳遞的。LocalPictureChoose提供選擇相冊圖片的能力,HttpPictureChoose提供選擇網絡圖片的能力。

       LocalPictureChoose提供的功能是將相冊中的所有圖片讀取出來,作成縮略圖排列在UI上,而後將每一個縮略圖綁定一個觸摸監聽器,一旦使用者選中某個縮略圖,就會將這個縮略圖對應的原圖uri放在intent中傳給CropPicture。具體代碼以下:

private void showImage() {
    DataAbilityHelper helper = DataAbilityHelper.creator(this);
    try {
        // columns爲null,查詢記錄全部字段,當前例子表示查詢id字段
        ResultSet resultSet = helper.query(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI, new String[]{AVStorage.Images.Media.ID}, null);
        while (resultSet != null && resultSet.goToNextRow()) {
            //建立image用以顯示系統相冊縮略圖
            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的功能主要是將用戶輸入的網絡圖片地址解析爲Uri傳遞給CropPicture,目前只支持手動輸入地址。

三、MainMenu

      一個簡單的主菜單界面,用戶能夠經過點擊不一樣的按鈕選擇裁剪相冊圖片仍是網絡圖片。

Library解析

        鴻蒙和安卓存在較多的能力差別,即兩者在實現同一 種功能時,方法不一樣,這不只體如今工程結構上,也體如今具體的代碼邏輯中。如下將對uCrop_ohos和uCrop的工程結構進行對比,並介紹幾個在uCrop_ohos移植過程當中遇到的安卓和鴻蒙的能力差別。

一、工程結構對比

鴻蒙開源第三方組件——uCrop_ohos圖片裁剪組件鴻蒙開源第三方組件——uCrop_ohos圖片裁剪組件

圖 6 uCrop_ohos(上)與uCrop(下)的工程結構對比

       能夠看出uCrop_ohos相比uCrop少封裝了一層Activity與Fragment,緣由有3個:

     (1)安卓的Activity與鴻蒙的Ability仍是有差異的,強行復現會致使代碼複用率低。

     (2)這一層與UI強耦合,因爲鴻蒙尚不支持安卓中許多控件,例如Menu等,這就致使難以原樣復現UCropActivity中的UI。

     (3)封裝程度越高,可供開發者自定義的程度就越小。

二、能力差別

    (1)圖片加載&保存

      不管是加載網絡圖片仍是相冊圖片,在uCrop和uCrop_ohos內部都是經過解析圖片的Uri實現的,因此須要有一個識別Uri種類的過程,即經過分析Uri的Scheme來實現Uri的分類。若是Uri的Scheme是http或https則會被認爲是網絡圖片,調用okhttp3的能力執行下載操做;若是Uri的Scheme是content(安卓)或dataability(鴻蒙)就會被認爲是本地圖片,執行復制操做。下載或複製的圖片將做爲被裁剪的圖片。代碼以下所示:

private void processInputUri() throws NullPointerException, IOException {
    String inputUriScheme = mInputUri.getScheme();
    //Scheme爲http或https即爲網絡圖片,執行下載
    if ("http".equals(inputUriScheme) || "https".equals(inputUriScheme)) {
        try {
            downloadFile(mInputUri, mOutputUri);
        } catch (NullPointerException e) {
            LogUtils.LogError(TAG, "Downloading failed:"+e);
            throw e;
        }
    //安卓中Scheme爲content即爲本地圖片,執行復制
    } else if ("content".equals(inputUriScheme)) {
        try {
            copyFile(mInputUri, mOutputUri);
        } catch (NullPointerException | IOException e) {
            LogUtils.LogError(TAG, "Copying failed:"+e);
            throw e;
        }
    //鴻蒙中Scheme爲dataability即爲本地圖片,執行復制
    } else if("dataability".equals(inputUriScheme)){
        try {
            copyFile(mInputUri, mOutputUri);
        } catch (NullPointerException | IOException e) {
            LogUtils.LogError(TAG, "Copying failed:"+e);
            throw e;
        }

       圖片文件準備完成後,還須要將其解碼成Bitmap(安卓)或PixelMap(鴻蒙)格式以便實現uCrop後續的各類功能。在解碼以前還須要經過Uri來獲取文件流,在這一點上安卓和鴻蒙的實現原理不一樣。對於安卓,能夠經過openInputStream()函數得到輸入文件流InputStream:

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

     對於鴻蒙則須要調用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"寫權限
FileDescriptor fd = helper.openFile(uri, "w");
OutputStream outputStream = new FileOutputStream(fd);

(2)裁剪的實現

       在安卓版的uCrop中,裁剪功能的實現原理是將原圖(位圖1)位於裁剪框內的部分建立一個新的位圖(位圖2),而後將新的位圖保存成圖片文件(圖片文件1)。如圖7所示:

鴻蒙開源第三方組件——uCrop_ohos圖片裁剪組件

圖 7 uCrop裁剪功能的實現方法

       而在鴻蒙版uCrop_ohos中,裁剪功能的實現原理髮生了變化。鴻蒙系統API雖不支持對位圖的旋轉操做,但圖像的解碼API提供了旋轉能力,因此鴻蒙的裁剪過程是這樣的:

       首先將原圖(位圖1)保存爲一個臨時的圖片文件(圖片文件1),經過相對旋轉角度對臨時圖片文件進行讀取,此時讀取出的位圖(位圖2)就包含了正確的旋轉信息。而後再經過相對縮放和位移建立一個新的位圖(位圖3),這個位圖還會由於API的特性發生壓縮和錯切等形變,因此還須要再建立最後一個位圖(位圖4)來修正形變,最後再將位圖4保存成圖片文件(圖片文件2)。如圖8所示:

鴻蒙開源第三方組件——uCrop_ohos圖片裁剪組件

圖 8 uCrop_ohos裁剪功能的實現方法

(3)異步任務處理

        因爲圖片的讀取、裁剪和保存這些操做都是比較消耗系統性能的,直接致使的問題就是卡頓,因此須要使用異步任務將這些操做放到後臺操做,減小UI線程的負擔。下面以裁剪任務爲例進行介紹。

       在uCrop中使用的是BitmapCropTask類繼承AsyncTask類的方法:

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

而後在其中重寫doInBackground()和onPostExecute()函數,分別實現後臺裁剪任務的處理與回調:

@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修改了後臺任務的處理方案,首先將後臺任務的處理與回調合並寫在一個Runnable中,而後鴻蒙原生的多線程處理機制EventHandler搭配EventRunner新開一個線程用於處理這個Runnable,實現了圖片裁剪任務的異步處理。

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();
            }
        }
    });
}

項目貢獻人

        吳聖垚 鄭森文 朱偉 陳美汝 王佳思 

做者:朱偉ISRC

想了解更多內容,請訪問51CTO和華爲合做共建的鴻蒙社區:harmonyos.51cto.com

相關文章
相關標籤/搜索