Flutter : 画像の切り抜き

Flutter + Firebase でアプリ作成のためのパーツあつめ。

まで、Android、iOSでの簡易動作確認ができたので、画像切り抜きパッケージ、image_cropper を試す。

1.例によってエラー対応

pub.devのimage_cropperページのExampleに従い、コードを記述し実行するが、androidx.core.widget.TintableCompoundDrawableView が存在しないエラー

I/pea.favo_phras( 6224): Rejecting re-init on previously-failed class java.lang.Class: java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/core/widget/TintableCompoundDrawablesView;
I/pea.favo_phras( 6224):   at java.lang.Object java.lang.Class.newInstance() (Class.java:-2)
I/pea.favo_phras( 6224):   at android.app.Activity android.app.AppComponentFactory.instantiateActivity(java.lang.ClassLoader, java.lang.String, android.content.Intent) (AppComponentFactory.java:69)
I/pea.favo_phras( 6224):   at android.app.Activity androidx.core.app.CoreComponentFactory.instantiateActivity(java.lang.ClassLoader, java.lang.String, android.content.Intent) (CoreComponentFactory.java:43)

image_cropper のGithubのissueに同じ事象の報告および解決策情報あり。

https://github.com/hnvn/flutter_image_cropper/issues/78

build.gradle の dependencies に以下を追記

implementation 'androidx.core:core:1.0.2'
implementation 'androidx.appcompat:appcompat:1.0.2'

とのことだが、それだけでは解決しないため、エラーログを見ながら以下のように調整。

AndroAndroidXの互換性対応で、この手のエラー解決のコツがなんとなくつかめてきたが、、、若干不安。

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.google.firebase:firebase-analytics:16.0.0'
    implementation 'androidx.core:core:1.0.0'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        print(details.requested.group)
        if (details.requested.group ==~ /^androidx\.(core|viewpager|drawlayout|interpolator|fragment|versionedparcelable|appcompat)/
        ) {
            details.useVersion '1.0.0'
            details.because 'API needs higher versions'
        }
        if (details.requested.group == 'androidx.lifecycle'
            ) {
            details.useVersion '2.1.0'
            details.because 'API needs higher versions'
        }
    }
}

2.Flutter

上記エラー対応によって、pub.devのimage_cropperページのExampleのコードが動いた。

2.1 pubspec.yaml(/)

に以下を追記

dependencies:
    :
  image_cropper: ^1.1.0

2.2 AndroidManifest.xml(/android/app/main/res)

に以下のActivityを追加。

<application>
    <activity
        android:name="com.yalantis.ucrop.UCropActivity"
        android:screenOrientation="portrait"
        android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
</application>

2.3 ソースコード

import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';

class SecondRoute extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _SecondRoute();
  }
}
class _SecondRoute extends State<SecondRoute>{
  Image _image = Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg');

  Future _handleImageCrop(BuildContext context) async {

    var imageFile = await ImagePicker.pickImage(source: ImageSource.camera);
    var croppedFile = await ImageCropper.cropImage(
        sourcePath: imageFile.path,
        aspectRatioPresets: [
          CropAspectRatioPreset.square,
          CropAspectRatioPreset.ratio3x2,
          CropAspectRatioPreset.original,
          CropAspectRatioPreset.ratio4x3,
          CropAspectRatioPreset.ratio16x9
        ],
        androidUiSettings: AndroidUiSettings(
            toolbarTitle: 'Cropper',
            toolbarColor: Colors.deepOrange,
            toolbarWidgetColor: Colors.white,
            initAspectRatio: CropAspectRatioPreset.original,
            lockAspectRatio: false),
        iosUiSettings: IOSUiSettings(
          minimumAspectRatio: 1.0,
        )
    );
    setState(() {
      _image = Image.file(croppedFile);
    });
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints viewportConstraints) {
        return SingleChildScrollView(
          child: ConstrainedBox(
            constraints: BoxConstraints(
              minHeight: viewportConstraints.maxHeight,
            ),
            child: IntrinsicHeight(
              child: Column(
                children: <Widget>[
                  Container(
                    // A fixed-height child.
                    color: const Color.fromARGB(255, 255, 255, 255),
                    height: 120.0,
                  ),
                  Expanded(
                    // A flexible child that will grow to fit the viewport but
                    // still be at least as big as necessary to fit its contents.
                    child: Container(
                      color: const Color(0xffffffff),
                      height: 240.0,
                      constraints: BoxConstraints(
                          minWidth: viewportConstraints.maxWidth,
                          minHeight: double.infinity
                      ),
                      child: Column(
                        children: <Widget>[
                          RaisedButton(
                            onPressed: () {
                              _handleImageCrop(context);
                            },
                            child: Text('Image Crop'),
                          ),
                          RaisedButton(
                            onPressed: () {
                              Navigator.pop(context);
                            },
                            child: Text('Back'),
                          ),
                          _image,
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }

}

3.実行

3.1 Android

「Image Crop」ボタン押下で、_handleImageCrop() を呼び出し、画像選択 を経由(カメラを起動)して取得したファイル

var imageFile = await ImagePicker.pickImage(source: ImageSource.camera);

を、

var croppedFile = await ImageCropper.cropImage()

に引き渡す。

flutter_image_crop01

画像を切り抜く

flutter_image_crop02

範囲を選択し、画面右上のチェック

flutter_image_crop03

切り抜かれた範囲を画面に表示。OK!!

flutter_image_crop04

3.2 iOS

Android同様のことをiPhoneエミュレーターで。

特に変更はなし。

・・・エミュレータのため、カメラ起動できないようで、OKでエラーになるので、

var imageFile = await ImagePicker.pickImage(source: ImageSource.camera);

var imageFile = await ImagePicker.pickImage(source: ImageSource.gallery);

に変更して確認

flutter_image_crop_ios01

Android同様に画像の切り抜きUIが起動。

flutter_image_crop_ios02

Doneで、選択部分が画面表示された!

flutter_image_crop_ios03

Flutterもエラーの嵐でうんざりしかけたが、AndroAndroidXの互換性対応で、何とか乗り切れそうか!?

macの操作がぎこちないので、本でも買おうかしら。



Flutter : 画像の選択

Flutter + Firebase でアプリ作成のためのパーツあつめ。

画像選択のパッケージ、image_picker を試す。

https://pub.dev/packages/image_picker

のだが、AndroidX互換性問題に遭遇し難儀したが、何とか解決

1.Flutter

1.1 pubspec.yaml (/) にimage_pickerを追記

dependencies:
    :
  image_picker: ^0.6.1+8

1.2 second.dart

画面遷移で起動した画面に、画像選択し、画像表示するウィジェットを追加する (関係ないコードは除外)

ウィジェットのルートにScaffoldを使用したままだと、写真を選択したときにサイズがはみ出ると、下図のように、はみで多分に黒と黄色の車線がはいって表示され、以下のようなエラーログが吐かれる。

════════ Exception caught by rendering library ═════════════════════════════════════════════════════
The following assertion was thrown during layout:
A RenderFlex overflowed by 236 pixels on the bottom.

flutter_pick_image_err

スクロールできるようにする必要があるようなので、以下のサンプルソースに合わせて、スクロールビューの中に入れる。

https://api.flutter.dev/flutter/widgets/SingleChildScrollView-class.html

import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';


class SecondRoute extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _SecondRoute();
  }
}
class _SecondRoute extends State<SecondRoute>{
  Image _image = Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg');

  Future _handleImagePick(BuildContext context) async {
    var imageFile = await ImagePicker.pickImage(source: ImageSource.gallery);
    var image = Image.file(imageFile);
    setState(() {
      _image = image;
    });
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints viewportConstraints) {
        return SingleChildScrollView(
          child: ConstrainedBox(
            constraints: BoxConstraints(
              minHeight: viewportConstraints.maxHeight,
            ),
            child: IntrinsicHeight(
              child: Column(
                children: <Widget>[
                  Container(
                    // A fixed-height child.
                    color: const Color.fromARGB(255, 255, 255, 255),
                    height: 120.0,
                  ),
                  Expanded(
                    // A flexible child that will grow to fit the viewport but
                    // still be at least as big as necessary to fit its contents.
                    child: Container(
                      color: const Color(0xffffffff),
                      height: 240.0,
                      constraints: BoxConstraints(
                        minWidth: viewportConstraints.maxWidth,
                        minHeight: double.infinity
                      ),
                      child: Column(
                        children: <Widget>[
                          RaisedButton(
                            onPressed: () {
                              _handleImagePick(context);
                            },
                            child: Text('Image Pick'),
                          ),
                          _image,
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }

}

2.Android

画像の初期表示には、Flutter Imageクラスのサンプル画像を表示。

flutter_pick_image01

「Image Pick」ボタン押下でギャラリーから画像を選択できる。

var imageFile = await ImagePicker.pickImage(source: ImageSource.gallery);

のImageSource を、ImageSource.camera に変更することで、カメラが起動する!

flutter_pick_image02

選択すると、Image コンポーネントに選択された画像がロードされる!

flutter_pick_image03

3.iOS

Androidに対応して、少し変更する必要がある。

info.plist(/ios/Runner) に以下を追記。それぞれの機能(key)を利用するときに、目的(string)を表示しユーザーに許可を求める。

<dict>
    <key>NSPhotoLibraryUsageDescription</key>
    <string>What purpose to use</string>
    <key>NSCameraUsageDescription</key>
    <string>What purpose to use</string>
    <key>NSMicrophoneUsageDescription</key>
    <string>What purpose to use</string>
    <key>CFBundleDevelopmentRegion</key>
    :
</dict>

エミュレーターで実行。Androidと同様に表示された。

flutter_pick_image_ios01

「Image Pick」ボタンで、ユーザーに確認。OK押下。

flutter_pick_image_ios02

ギャラリー?が表示されるので、画像を選択。

flutter_pick_image_ios03

選択した画像が、表示された!

flutter_pick_image_ios04

AndroidX対応で、結構はまったが、行けそうか!?

iOSについても勉強する必要あるな。

1.パッケージエラーでまくり

XamarinからFlutterに乗り換えをもくろんで環境構築画面遷移や、Webアクセスなど必要な機能を少しずつ試していたのだが、画像選択のために image_picker パッケージを利用しようとしたところ以下のようなエラーでまくりで、ちょっと萎える。

Running "flutter pub get" in favo_phrase...                         0.9s
Launching lib\main.dart on MAR LX2J in debug mode...
Initializing gradle...
Resolving dependencies...
Running Gradle task 'assembleDebug'...
registerResGeneratingTask is deprecated, use registerGeneratedResFolders(FileCollection)
registerResGeneratingTask is deprecated, use registerGeneratedResFolders(FileCollection)
registerResGeneratingTask is deprecated, use registerGeneratedResFolders(FileCollection)

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:processDebugResources'.
> Android resource linking failed
  Output:  C:\workspaces\FavoPhrase-workspace\trial\favo_phrase\build\app\intermediates\incremental\mergeDebugResources\merged.dir\values\values.xml:37: error: resource android:attr/fontVariationSettings not found.
  C:\workspaces\FavoPhrase-workspace\trial\favo_phrase\build\app\intermediates\incremental\mergeDebugResources\merged.dir\values\values.xml:37: error: resource android:attr/ttcIndex not found.
  error: failed linking references.

  Command: C:\Users\Hiroto Yagi\.gradle\caches\transforms-1\files-1.1\aapt2-3.2.1-4818971-windows.jar\7e99101b246de0c19b256be23ae2fa31\aapt2-3.2.1-4818971-windows\aapt2.exe link -I\
          C:\Programs\Android\Sdk\platforms\android-27\android.jar\
          --manifest\
          C:\workspaces\FavoPhrase-workspace\trial\favo_phrase\build\app\intermediates\merged_manifests\debug\processDebugManifest\merged\AndroidManifest.xml\
          -o\
          C:\workspaces\FavoPhrase-workspace\trial\favo_phrase\build\app\intermediates\processed_res\debug\processDebugResources\out\resources-debug.ap_\
          -R\
          @C:\workspaces\FavoPhrase-workspace\trial\favo_phrase\build\app\intermediates\incremental\processDebugResources\resources-list-for-resources-debug.ap_.txt\
          --auto-add-overlay\
          --java\
          C:\workspaces\FavoPhrase-workspace\trial\favo_phrase\build\app\generated\not_namespaced_r_class_sources\debug\processDebugResources\r\
          --custom-package\
          info.typea.favo_phrase\
          -0\
          apk\
          --output-text-symbols\
          C:\workspaces\FavoPhrase-workspace\trial\favo_phrase\build\app\intermediates\symbols\debug\R.txt\
          --no-version-vectors
  Daemon:  AAPT2 aapt2-3.2.1-4818971-windows Daemon #0
  Output:  C:\Users\Hiroto Yagi\.gradle\caches\transforms-1\files-1.1\core-1.0.2.aar\f850178738aab58396b10a1f10703ed8\res\values\values.xml:89:5-125:25: AAPT: error: resource android:attr/fontVariationSettings not found.

  C:\Users\Hiroto Yagi\.gradle\caches\transforms-1\files-1.1\core-1.0.2.aar\f850178738aab58396b10a1f10703ed8\res\values\values.xml:89:5-125:25: AAPT: error: resource android:attr/ttcIndex not found.

  error: failed linking references.
  Command: C:\Users\Hiroto Yagi\.gradle\caches\transforms-1\files-1.1\aapt2-3.2.1-4818971-windows.jar\7e99101b246de0c19b256be23ae2fa31\aapt2-3.2.1-4818971-windows\aapt2.exe link -I\
          C:\Programs\Android\Sdk\platforms\android-27\android.jar\
          --manifest\
          C:\workspaces\FavoPhrase-workspace\trial\favo_phrase\build\app\intermediates\merged_manifests\debug\processDebugManifest\merged\AndroidManifest.xml\
          -o\
          C:\workspaces\FavoPhrase-workspace\trial\favo_phrase\build\app\intermediates\processed_res\debug\processDebugResources\out\resources-debug.ap_\
          -R\
          @C:\workspaces\FavoPhrase-workspace\trial\favo_phrase\build\app\intermediates\incremental\processDebugResources\resources-list-for-resources-debug.ap_.txt\
          --auto-add-overlay\
          --java\
          C:\workspaces\FavoPhrase-workspace\trial\favo_phrase\build\app\generated\not_namespaced_r_class_sources\debug\processDebugResources\r\
          --custom-package\
          info.typea.favo_phrase\
          -0\
          apk\
          --output-text-symbols\
          C:\workspaces\FavoPhrase-workspace\trial\favo_phrase\build\app\intermediates\symbols\debug\R.txt\
          --no-version-vectors
  Daemon:  AAPT2 aapt2-3.2.1-4818971-windows Daemon #0

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 19s
*******************************************************************************************
The Gradle failure may have been because of AndroidX incompatibilities in this Flutter app.
See https://goo.gl/CP92wY for more information on the problem and how to fix it.
*******************************************************************************************
Finished with error: Gradle task assembleDebug failed with exit code 1

どうやら原因はAndroidXの互換性問題のようなので、適当に対処してみるが解決せず。AndroidXの理屈がわからないと解決しなさそう。しばらくAndroid触ってなかったのでAndroidX?状態なので調べる。

2.AndroidX

https://developer.android.com/jetpack/androidx?hl=ja

2.1 概要

  • Jetpack内で使用されているオープンソースプロジェクト。
    • Androidアプリを開発のためのライブラリ、ツール、ガイダンス
    • androidx.* パッケージライブラリで構成
  • 元の、Android Support Libraryを大幅に改良。
  • Android OSとは別個にリリースされ、下位互換性を提供
    • androidx で始まる名前空間に属する
    • Andrpod Support Library は androidxへ移行 対応一覧

2.2 使用する

  • AndroidXを使用する場合は、Compile SDKを Android9.0(API レベル28)以上に設定
  • Android Support Libraryを移行、パッケージ名とMavenのアーティファクト名が変更
    • クラス、メソッド、フィールドは変更せず
  • 既存のプロジェクトの移行はAndroid Studioを利用するのが吉
    • メニュー - Refactor - Migrate to AndroidX
  • AndroidXに移行されていないMaven依存関係がある場合、gradle.properties ファイルに以下を設定することで、Android Studioのビルドシステムが自動的に移行する
    • android.useAndroidX=true
      • Support Libraryではなく、該当するAndroidXを使用する
    • android.enableJetifier=true
      • ライブラリのバイナリをAndroidXを使用するように書き換える

3.対処(1)

だいたい理屈はわかったので、以下の対処

https://flutter.dev/docs/development/androidx-migration

3.1 File - Project Structure から、API レベルを28にする。SDKがダウンロードされてなければする。

flutter_androidx00

3.2 build.gradle(/android/app)) を編集

compileSdkVersion および、targetSdkVersion を28に変更

android {
    compileSdkVersion 28
     :
    defaultConfig {
          :
        targetSdkVersion 28
          :
    }
          :
}

3.3 gradle.properties(/android)に以下を追記

android.useAndroidX=true
android.enableJetifier=true

4.実行

4.1 pubspec.yaml(/) のdependencies に image_pickerimage_cropper を追記し実行

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^0.1.2
  firebase_core: ^0.4.0+9
  firebase_auth: ^0.14.0+5
  google_sign_in: ^4.0.7
  image_picker: ^0.6.1+8
  image_cropper: ^1.1.0

4.2 以下のエラー

Android dependency 'androidx.fragment:fragment' has different version for the compile (1.0.0-rc01) and runtime (1.1.0) classpath. You should manually set the same version via DependencyResolution

うーんまだエラー出るか。。。

なかなか難儀。コンパイルとランタイムでバージョンが違うから、DependencyResolution で手動でバージョン設定しなさいと。

5.対処(2)

5.1 build.gradle(/android/app)に追記

https://github.com/flutter/flutter/issues/27254

https://qiita.com/sekitaka_1214/items/2bfa89286ebe2c08a6d8

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        print(details.requested.group)
        if (details.requested.group ==~ /^androidx\.(core|viewpager|drawlayout|interpolator|fragment|versionedparcelable)/
        ) {
            details.useVersion '1.0.0'
            details.because 'API needs not pre-release versions'
        }
        if (details.requested.group == 'androidx.lifecycle'
            ) {
            details.useVersion '2.1.0'
            details.because 'API needs not pre-release versions'
        }
    }
}

成功!

これで、image_picker パッケージを使った簡単なサンプルをAndroidおよびiPhoneエミュレーター上で動かすことが出来ました。

まぁよかったけど、環境設定で、時間もかかりすぎるし、こういうのなんとかならないかな。。。

やりたいことが出来るころには疲れ果てるわ。

書籍買うと全部うまいこと書いてくれてるのかな~?