DUICUO

HarmonyOS オープンソースサードパーティコンポーネント - uCrop_ohos 画像切り抜きコンポーネント

[[391927]]

詳細については、以下をご覧ください。

51CTOとHuaweiが共同で構築したHarmonyOSテクノロジーコミュニティ。

https://harmonyos..com

序文

Androidプラットフォームベースの画像クロッピングコンポーネントであるuCrop(https://github.com/Yalantis/uCrop)が、HarmonyOSへの移行とリファクタリングを完了しました。コードはオープンソース(https://gitee.com/isrc_ohos/u-crop_ohos)になりました。ぜひダウンロードしてご利用いただき、貴重なフィードバックをお寄せください。

背景

uCropは、画像の拡大縮小と切り抜きをサポートするオープンソースの画像切り抜きライブラリです。Androidプラットフォームで人気のコンポーネントで、GitHubでは10,000以上のスターと2,000近くのフォークを誇っています。uCropは、高度なカプセル化、スムーズな操作性、そして高度なカスタマイズ性を特徴としており、様々なアプリで広く利用されています。

コンポーネント効果表示

AndroidとHarmonyOSのUIコンポーネントは大きく異なります。uCrop_ohosの実装はAndroid版uCropのUIを完全に再構築するため、uCrop_ohosのコンポーネントはuCropとは見た目が全く異なります。

このコンポーネントの効果は、画像の選択と画像の切り抜きという2つのステップで実証できます。以下では、これらを順に説明し、実演します。

1. uCrop_ohos画像選択

uCrop_ohosは、アルバムまたはオンラインから選択した画像のトリミングをサポートしています。ユーザーは、図1に示すように、メインメニューから対応する機能を選択できます。

図1 メインメニューインターフェース

(1)uCrop_ohosはアルバムから写真を読み込みます

ユーザーがコンポーネントに適切な権限を付与すると、uCrop_ohos はスマートフォンのフォトアルバム内のすべての画像を自動的に読み取り、サムネイルを UI 上にリストとして表示します。ユーザーはリストを上下にスクロールして目的の画像を見つけることができます(図 2 を参照)。サムネイルをクリックすると、uCrop_ohos のクロッピングインターフェースにリダイレクトされ、以降の操作を実行できます。

図2 システムアルバムから写真を選択する

(2)uCrop_ohosはネットワーク画像を読み込む

図 3 に示すように、ユーザーは入力ボックスに画像の URL を入力し、[OK] ボタンをクリックする必要があります。uCrop_ohos は自動的に画像をダウンロードし、後続の操作を実行するためにクロッピング インターフェイスにリダイレクトします。

図3 オンライン画像の選択

2. uCrop_ohos 画像の切り抜き

図4 uCrop_ohosクロッピングインターフェース

図4はuCrop_ohosのクロッピングインターフェースを示しています。ユーザーはジェスチャー操作で画像のズーム、回転、パンを行ったり、ボタン、スライダー、その他のコントロールを使って対応する操作を実行したりできます。画像が適切な状態に調整されたら、クロップボタンをクリックすると、切り抜かれた新しい画像が取得され、スマートフォンのフォトアルバムに保存されます。さらに、このコンポーネントは画像とクロッピングフレーム間の適応機能を備えており、クロッピングフレームが常に画像領域内に収まるようにすることで、クロッピングフレームが画像サイズを超えることによる一連の問題を防止します。

サンプル分析

図5. サンプルのエンジニアリング構造

uCrop_ohos のコア機能はすべてライブラリによって提供されます。サンプルは主にUIの構築とライブラリのインターフェースの呼び出しに使用されます。図5に示すように、サンプルプロジェクトの構造は比較的シンプルで、4つのファイルで構成されています。これらについては後ほど詳しく説明します。

1. 切り抜き画像

CropPicture ファイルはクロッピングインターフェースを提供します。このインターフェースの主なロジックは、画像 URI を使用してライブラリ内の UCropView クラスをインスタンス化することです。uCrop_ohos はまずユーザーが選択したオリジナル画像のコピーを作成し、そのコピーに対してクロッピングを実行するため、UCropView に画像を渡すには 2 つの URI が必要です。1 つはインテントから取得される uri_i という URI で、ユーザーが選択したオリジナル画像(ローカル画像またはネットワーク画像)を識別します。もう 1 つは uri_o という URI で、オリジナル画像のコピー(ローカル画像である必要があります)を識別します。コードは次のとおりです。

  1. //URI_IN
  2. URI uri_i = intent.getUri();
  3.  
  4. //URI_OUT
  5. 文字列ファイル名 = "test.jpg" ;
  6. PixelMap.InitializationOptions オプション = 新しい PixelMap.InitializationOptions();
  7. options.size = 新しいサイズ(100,100);
  8. PixelMap pixelmap = PixelMap.create (options);
  9. URI uri_o = saveImage(ファイル名、ピクセルマップ);
  10.  
  11. //Uクロップビュー
  12. UCropView uCropView = 新しい UCropView(this);
  13. 試す {
  14. uCropView.getCropImageView().setImageUri(uri_i, uri_o);
  15. uCropView.getOverlayView().setShowCropFrame( true );
  16. uCropView.getOverlayView().setShowCropGrid( true );
  17. uCropView.getOverlayView().setDimmedColor(Color.TRANSPARENT.getValue());
  18.  
  19. } キャッチ (例外 e) {
  20. e.printStackTrace();
  21. }

ライブラリは開発者に公開インターフェースを提供しており、開発者は独自のUI機能を簡単にカプセル化できます。例えば、このファイル内の回転スライダーとスケールスライダー、回転ボタンとスケールボタン、そして現在の回転状態とスケール状態の表示はすべて、ライブラリインターフェースを呼び出すことで実装されています。次の実装を例に挙げましょう。ユーザーがボタンをタッチすると、画像が右に90度回転します。このボタンのコア機能は、ライブラリ内の非常にシンプルな`postRotate()`関数を呼び出すことで実現されています。

  1. //ボタンを右に90度回転
  2. ボタン button_plus_90 = new Button(this);
  3. button_plus_90.setText( "+90°" );
  4. button_plus_90.setTextSize(80);
  5. button_plus_90.setBackground(ボタンの背景);
  6. button_plus_90.setClickedListener(新しいComponent.ClickedListener() {
  7. @オーバーライド
  8. パブリックvoid onClick(コンポーネント コンポーネント) {
  9. 浮動小数点数度 = 90f;
  10. //回転の中心を計算する
  11. float center_X = uCropView.getOverlayView().getCropViewRect().getCenter().getPointX();
  12. フロートcenter_Y = uCropView.getOverlayView().getCropViewRect().getCenter().getPointY();
  13. //回転
  14. uCropView.getCropImageView().postRotate(度、center_X、center_Y);
  15. // 適応
  16. uCropView.getCropImageView().setImageToWrapCropBounds( false );
  17. //回転角度を表示する
  18. mDegree = uCropView.getCropImageView().getCurrentAngle();
  19. text.setText( "現在の回転角度: " + df.format(mDegree) + " °" );
  20. }
  21. });

2. LocalPictureChoose と HttpPictureChoose

前述の通り、uri_i は LocalPictureChoose または HttpPictureChoose によって渡されるインテントを通じて取得されます。LocalPictureChoose はアルバムから画像を選択する機能を提供し、HttpPictureChoose は Web ブラウザから画像を選択する機能を提供します。

LocalPictureChoose はアルバムからすべての画像を読み取り、サムネイルを作成し、UI 上に配置します。各サムネイルはタッチリスナーにバインドされます。ユーザーがサムネイルを選択すると、対応する元の画像の URI がインテントを通じて CropPicture に送信されます。具体的なコードは次のとおりです。

  1. プライベートvoid showImage() {
  2. DataAbilityHelper ヘルパー = DataAbilityHelper.creator(this);
  3. 試す {
  4. // 列がnullの場合、クエリはレコードのすべてのフィールドを取得します。この例では、 id フィールドを取得することを意味します。
  5. 結果セット resultSet = helper.query(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI、新しい文字列[]{AVStorage.Images.Media.ID}、 null );
  6. while (resultSet != null && resultSet.goToNextRow()) {
  7. // システムフォトアルバムのサムネイルを表示するための画像を作成します。
  8. ピクセルマップ ピクセルマップ = null ;
  9. イメージソース imageSource = null ;
  10. 画像 image = new Image(this);
  11. 画像の幅を250に設定します。
  12. イメージの高さを250に設定します。
  13. image.setLeftAndRight(10, 10);
  14. image.setTopAndBottom(10, 10);
  15. image.setScaleMode(Image.ScaleMode.CLIP_CENTER);
  16. // idフィールドの値を取得する
  17. int id = resultSet.getInt(resultSet.getColumnIndexForName(AVStorage.Images.Media.ID));
  18. URI uri = Uri.appendEncodedPathToUri(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI、String.valueOf(id));
  19. ファイル記述子 fd = helper.openFile(uri, "r" );
  20. ImageSource.DecodingOptions デコーディングオプション = 新しい ImageSource.DecodingOptions();
  21. 試す {
  22. // 画像をデコードして画像フォルダに配置します。
  23. imageSource = ImageSource.create (fd, null );
  24. ピクセルマップ = imageSource.createPixelmap( null );
  25. int高さ = pixelMap.getImageInfo()。サイズ.height;
  26. int幅 = pixelMap.getImageInfo()。サイズ.width;
  27. float sampleFactor = Math.max ( height /250f, width/250f);
  28. デコードオプション.desiredSize = 新しいサイズ(( int ) (幅/サンプルファクター), ( int )(高さ/サンプルファクター));
  29. ピクセルマップ = imageSource.createPixelmap(デコーディングオプション);
  30. } キャッチ (例外 e) {
  31. e.printStackTrace();
  32. ついに {
  33. if (imageSource != null ) {
  34. imageSource.release();
  35. }
  36. }
  37. image.setPixelMap(ピクセルマップ);
  38. image.setClickedListener(新しいComponent.ClickedListener() {
  39. @オーバーライド
  40. パブリックvoid onClick(コンポーネント コンポーネント) {
  41. gotoCrop(uri);
  42. }
  43. });
  44. tableLayout.addComponent(画像);
  45. }
  46. } キャッチ (DataAbilityRemoteException | FileNotFoundException e) {
  47. e.printStackTrace();
  48. }
  49. }
  50. //URIはインテント内に配置される
  51. プライベートvoid gotoCrop(Uri uri){
  52. インテント intent = new Intent();
  53. intent.setUri(uri);
  54. 新しい CropPicture() を提示します (インテント);
  55. }

HttpPictureChooseの主な機能は、ユーザーが入力したネットワーク画像アドレスをURIに変換し、CropPictureに渡すことです。現在、アドレスの手動入力のみをサポートしています。

3. メインメニュー

シンプルなメイン メニュー インターフェイスにより、ユーザーはさまざまなボタンをクリックして、アルバム画像またはオンライン画像の切り抜きを選択できます。

ライブラリ分析

HarmonyOSとAndroidには大きな機能の違いがあり、同じ機能を異なる方法で実装しています。これはプロジェクト構造だけでなく、具体的なコードロジックにも反映されています。以下では、uCrop_ohosとuCropのプロジェクト構造を比較し、uCrop_ohosの移植プロセス中に発生したAndroidとHarmonyOSの機能の違いをいくつか紹介します。

1. 工学構造の比較

図6 uCrop_ohos(上)とuCrop(下)のエンジニアリング構造の比較

uCrop_ohos では、uCrop と比較して、Activity と Fragment 間のカプセル化レイヤーが 1 つ少ないことがわかります。これには次の 3 つの理由があります。

(1) AndroidのActivityとHarmonyOSのAbilityには依然として差異があり、これを無理やり再現するとコードの再利用率が低下します。

(2) このレイヤーはUIと強く結びついています。HarmonyOSはメニューなどAndroidの多くのコントロールをまだサポートしていないため、UCropActivityのUIを正確に再現することは困難です。

(3)カプセル化のレベルが高くなるほど、開発者が利用できるカスタマイズは少なくなります。

2. 能力の違い

(1)画像の読み込みと保存

uCropとuCrop_ohosは、ネットワークから画像を読み込む場合でも、フォトアルバムから読み込む場合でも、内部的に画像のURIを解析することでこれを実現しています。そのため、URIの種類を識別する処理、つまりURIのスキームを解析して分類する処理が必要です。URIのスキームがHTTPまたはHTTPSの場合、ネットワーク画像とみなされ、okhttp3機能を使用してダウンロード操作が実行されます。URIのスキームがcontent(Android)またはdataability(HarmonyOS)の場合、ローカル画像とみなされ、コピー操作が実行されます。ダウンロードまたはコピーされた画像は、トリミングされた画像として扱われます。コードを以下に示します。

  1. プライベートvoid processInputUri()はNullPointerException、IOExceptionをスローします{
  2. 文字列 inputUriScheme = mInputUri.getScheme();
  3. // スキームが HTTP または HTTPS の場合、Web イメージを表すので、ダウンロードを実行します。
  4. if ( "http" .equals(inputUriScheme) || "https" .equals(inputUriScheme)) {
  5. 試す {
  6. ダウンロードファイル(mInputUri、mOutputUri);
  7. } キャッチ (NullPointerException e) {
  8. LogUtils.LogError(TAG, "ダウンロードに失敗しました:" +e);
  9. eを投げる;
  10. }
  11. // Android では、Scheme が "content" に設定されている場合、イメージはローカルであることを意味します。コピー コマンドを実行します。
  12. }そうでなければ ( "content" .equals(inputUriScheme)) {
  13. 試す {
  14. コピーファイル(mInputUri、mOutputUri);
  15. } キャッチ (NullPointerException | IOException e) {
  16. LogUtils.LogError(TAG, "コピーに失敗しました:" +e);
  17. eを投げる;
  18. }
  19. //HarmonyOS では、Scheme はデータ可能性に設定されており、これはローカル イメージを意味し、コピーを実行します。
  20. }そうでない場合、if( "データ可能性" .equals(inputUriScheme)){
  21. 試す {
  22. コピーファイル(mInputUri、mOutputUri);
  23. } キャッチ (NullPointerException | IOException e) {
  24. LogUtils.LogError(TAG, "コピーに失敗しました:" +e);
  25. eを投げる;
  26. }

画像ファイルを準備したら、uCrop の様々な後続機能を有効にするために、Bitmap (Android) または PixelMap (HarmonyOS) 形式にデコードする必要があります。デコード前に、URI 経由でファイルストリームを取得する必要があります。Android と HarmonyOS では、この実装方法が異なります。Android では、openInputStream() 関数を使用して入力ファイルストリーム (InputStream) を取得できます。

  1. 入力ストリーム ストリーム = mContext.getContentResolver().openInputStream(mInputUri);

HarmonyOSでは、DataAbilityを呼び出す必要があります。まずDataAbilityHelperを使用してFileDescriptorを取得し、その後InputStreamを取得できます。

  1. 入力ストリーム ストリーム = null ;
  2. DataAbilityHelper ヘルパー = DataAbilityHelper.creator(mContext);
  3. ファイル記述子 fd = helper.openFile(mInputUri, "r" );
  4. ストリーム = 新しい FileInputStream(fd);

同様に、次のコードに示すように、Android と HarmonyOS では、画像の保存に必要な OutputStream を取得する方法が異なります。

  1. //AndroidでOutputStreamを取得する
  2. 出力ストリーム = context.getContentResolver().openOutputStream(Uri.fromFile(新しいファイル(mImageOutputPath)));
  3.  
  4. // HarmonyOSはOutputStreamを取得します
  5. valuesBucket.putInteger( "is_pending" , 1);
  6. DataAbilityHelper ヘルパー = DataAbilityHelper.creator(mContext.get());
  7. int id = ヘルパー。挿入(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI、valuesBucket);
  8. URI uri = Uri.appendEncodedPathToUri(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI、String.valueOf(id));
  9. // W書き込み権限が必要です
  10. ファイル記述子 fd = helper.openFile(uri, "w" );
  11. 出力ストリーム outputStream = 新しい FileOutputStream(fd);

(2)クリッピングの実施

Android版uCropでは、切り抜き機能は、元の画像(ビットマップ1)の切り抜き枠内の部分から新しいビットマップ(ビットマップ2)を作成し、それを画像ファイル(画像ファイル1)として保存することで機能します。図7をご覧ください。

図7. uCropクロッピング機能の実装方法

HarmonyOS版のuCrop_ohosでは、クロッピング機能の実装原理が変更されました。HarmonyOSシステムAPIはビットマップの回転操作をサポートしていませんが、画像デコードAPIは回転機能を提供しています。そのため、HarmonyOSにおけるクロッピング処理は以下のようになります。

まず、元の画像(ビットマップ1)を一時画像ファイル(画像ファイル1)として保存します。次に、この一時画像ファイルを指定された角度を基準に回転させて読み込みます。その結果得られるビットマップ(ビットマップ2)には、正しい回転情報が含まれます。次に、指定された角度を基準に画像を拡大縮小およびシフトすることで、新しいビットマップ(ビットマップ3)を作成します。このビットマップはAPIの特性により圧縮および歪みが生じるため、これらの歪みを補正するために最終的なビットマップ(ビットマップ4)を作成する必要があります。最後に、ビットマップ4を画像ファイル(画像ファイル2)として保存します。図8を参照してください。

図8. uCrop_ohosプルーニング関数の実装方法

(3)非同期タスク処理

画像の読み込み、切り抜き、保存といった操作は比較的リソースを消費し、遅延の直接的な原因となるため、これらの操作をバックグラウンドで実行する非同期タスクを導入し、UIスレッドの負荷を軽減する必要があります。以下のセクションでは、切り抜きを例に挙げます。

uCrop は、AsyncTask クラスから継承された BitmapCropTask クラスを使用します。

  1. パブリッククラス BitmapCropTask は AsyncTask<Void, Void, Throwable> を拡張します。

次に、doInBackground() 関数と onPostExecute() 関数をオーバーライドして、それぞれバックグラウンド クロッピング タスクとコールバックを処理します。

  1. @オーバーライド
  2. @Null可能
  3. 保護された Throwable doInBackground(Void... パラメータ) {
  4. mViewBitmap == null の場合{
  5. 新しい NullPointerException( "ViewBitmap は null です" )を返します
  6. }そうでない場合 (mViewBitmap.isRecycled()) {
  7. 新しい NullPointerException( "ViewBitmap はリサイクルされます" )を返します
  8. }そうでなければ (mCurrentImageRect.isEmpty()) {
  9. 新しい NullPointerException( "CurrentImageRect が空です" )を返します
  10. }
  11.  
  12. 試す {
  13. 作物();
  14. mViewBitmap = null ;
  15. } キャッチ (スロー可能なオブジェクト) {
  16. スロー可能なオブジェクトを返します
  17. }
  18.  
  19. 戻る ヌル;
  20. }
  21. @オーバーライド
  22. 保護されたvoid onPostExecute(@Nullable Throwable t) {
  23. mCropCallback != null の場合{
  24. t == null場合
  25. URI uri = Uri.fromFile(新しいファイル(mImageOutputPath));
  26. mCropCallback.onBitmapCropped(uri、cropOffsetX、cropOffsetY、mCroppedImageWidth、mCroppedImageHeight);
  27. }それ以外{
  28. mCropCallback.onCropFailure(t);
  29. }
  30. }
  31. }

HarmonyOSにはAndroidのAsyncTaskに相当するクラスがないため、uCrop_ohosはバックグラウンドタスク処理の仕組みを変更しました。まず、バックグラウンドタスク処理とコールバックをまとめてRunnableに記述します。次に、HarmonyOSネイティブのマルチスレッド処理機構であるEventHandlerとEventRunnerが連携して、このRunnableを処理するための新しいスレッドを生成することで、画像クロッピングタスクの非同期処理を実現しています。

  1. パブリックvoid doInBackground(){
  2. イベントランナー eventRunner = イベントランナー.create ();
  3. EventHandler ハンドラー = 新しい EventHandler(eventRunner);
  4. ハンドラー.postTask(新しい実行可能() {
  5. @オーバーライド
  6. パブリックvoid run() {
  7. mViewBitmap == null の場合{
  8. Throwable t = new NullPointerException( "ViewBitmap は null です" );
  9. mCropCallback.onCropFailure(t);
  10. 戻る;
  11. }そうでない場合 (mViewBitmap.isReleased()) {
  12. Throwable t = new NullPointerException( "ViewBitmap は null です" );
  13. mCropCallback.onCropFailure(t);
  14. 戻る;
  15. }そうでなければ (mCurrentImageRect.isEmpty()) {
  16. Throwable t = new NullPointerException( "ViewBitmap は null です" );
  17. mCropCallback.onCropFailure(t);
  18. 戻る;
  19. }
  20. 試す {
  21. 作物();
  22. mViewBitmap = null ;
  23. } キャッチ (IOException e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. });
  28. }

プロジェクト貢献者

ウー・シェンヤオ、ジェン・センウェン、ジュー・ウェイ、チェン・メイルー、ワン・ジアシ

詳細については、以下をご覧ください。

51CTOとHuaweiが共同で構築したHarmonyOSテクノロジーコミュニティ。

https://harmonyos..com