省エネエンジニア

Flutter、vue.js修行中。

【Flutter】Androidで以前作ったViewをflutterで実現する

こんにちは。
以前お仕事で下記のようなデザインをAndroidで作ったのですが、それをFlutterでどう実現できるかやってみました。
ランキングを色と番号で表現しています。

f:id:qkuroneko:20190718122715j:plain:w320

Androidで作った時もどう実現しようか悩んで最初は、Canvasに描くという方法をとりました。
なぜかというとランキング毎に色が違うので色だけ渡せばViewを使い回しできると思ったのです。

しかし、この方法だとListViewのスクロールがもたつく結果となり諦めました。

次にとったのが、xmlで半円のdrawableを色の数だけ作成し、描画時にランキングが何位かでxmlを変更するという方法です。

結局、ListViewがスムーズに動くのでこの方法に落ち着きました。
しかし、これは大変手間がかかり作るのがとても面倒でした。

さて、Flutterならどうすればいいでしょうか。

FlutterのContainerというWidgetにはdecorationをつけることができます。
Androidだと面倒だった円や丸角の背景が結構簡単につけられます。

 Container(
    width: 64.0,
    height: 64.0,
    decoration: BoxDecoration(color: Colors.blue, shape: BoxShape.circle),
)

f:id:qkuroneko:20190718124629j:plain

  Container(
    margin: EdgeInsets.symmetric(vertical: 8.0),
    width: 32.0,
    height: 64.0,
    decoration: BoxDecoration(
      color: Colors.blueAccent,
      borderRadius: BorderRadius.only(
          topRight: Radius.circular(32.0),
          bottomRight: Radius.circular(32.0)),
    ),
  )

borderRadiusで丸角をつけることができるのですが、全部を丸角にするのではなく一部にだけつけることができます。

上のコードのようにすることで、右側だけ丸い半円が描けます。

あとはリストのindexをとって、番号に準じて色を変えればいいだけなので簡単に実現できますね。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    List<Widget> widgetList = List();
    for (var i = 0; i < 50; i++) {
      widgetList.add(_content(i));
      widgetList.add(Divider(height: 1.0,));
    }

    return Scaffold(
        appBar: AppBar(
          // Here we take the value from the MyHomePage object that was created by
          // the App.build method, and use it to set our appbar title.
          title: Text(widget.title),
        ),
        body: ListView(
          children: widgetList,
        ));
  }

  Widget _content(int index) {
    return Container(
      child: Row(
        children: <Widget>[
          Stack(
            alignment: Alignment.centerLeft,
            children: <Widget>[
//              Container(
//                margin: EdgeInsets.symmetric(vertical: 8.0),
//                width: 33.0,
//                height: 65.5,
//                decoration: BoxDecoration(
//                    color: Colors.blue,
//                    borderRadius: BorderRadius.only(
//                        topRight: Radius.circular(32.0),
//                        bottomRight: Radius.circular(32.0)),),
//              ),
              Container(
                margin: EdgeInsets.symmetric(vertical: 8.0),
                width: 32.0,
                height: 64.0,
                decoration: BoxDecoration(
                  color: index == 0
                      ? Colors.red
                      : index == 1
                          ? Colors.blueAccent
                          : index == 2 ? Colors.amber : Colors.lime,
                  borderRadius: BorderRadius.only(
                      topRight: Radius.circular(32.0),
                      bottomRight: Radius.circular(32.0)),
                    boxShadow: [BoxShadow(color: Colors.black26, offset: Offset(0.0, 2.0))]
                ),
                child: Center(
                  child: Text(
                    '${index + 1}',
                    style: TextStyle(
                        color: index < 3 ? Colors.white : Colors.black),
                  ),
                ),
              )
            ],
          ),
          Container(
            margin: EdgeInsets.all(16.0),
            child: Text('aaaaaaaaaa'),
          ),
        ],
      ),
    );
  }
}

f:id:qkuroneko:20190718125507p:plain:w320

これでこんな感じに色を変更しつつランキングを表現できます。
影はboxShadowでつけることができます。

影ではなくborderをつけたいと思ったところがコメントアウトのところなのですが、どうやらborderRadiusをつけていると一部にだけborderをつけることができなくなるみたいです。
border: Border.all(color: Colors.blueAccent, width: 2.0)をつけると左側にもボーダーが表示されるのでいまいちですよね。

f:id:qkuroneko:20190718125911p:plain:w320

そこで、Containerを重ねて、下に少しだけ大きい半円を置いたらいいのでは?と考えました。

StackalignmentAlignment.centerLeftにして左側に揃えます。

それがこちら。

f:id:qkuroneko:20190718130145p:plain:w320

じゃっかん太さにムラがあるけど、ボーダーを細めにすれば気にならないレベルかなと思います。

個人的にはAndroidSDKで作るより簡単だったように感じました。

【iOS】SwiftUIチュートリアルやってみた

WWDC 2019 で SwiftUI が発表されましたね。

とりあえず使ってみたいですよね。

Xcode11

SwiftUIを使うにはXcode11 が必要みたいです。
ダウンロードはこちら。beta版をダウンロードできます。

https://developer.apple.com/download/

チュートリアルページはこちら。

developer.apple.com

実装

  • Single View Application -> Next
  • Use SwiftUIにチェック

f:id:qkuroneko:20190612161354p:plain:w420

import SwiftUI

struct ContentView : View {
    var body: some View {
        Text("Hello World!!!!!")
    }
}

f:id:qkuroneko:20190612163523p:plain:w320

  • テキストの色、フォントを変える
import SwiftUI

struct ContentView : View {
    var body: some View {
        Text("Hello World!!!!!")
        .font(.title)
        .color(.green)
    }
}

f:id:qkuroneko:20190612163822p:plain:w320

  • 横にプレビューが表示されるみたいですが、私のOSがまだMojave 10.14.5のままなので見てれないです

  • 縦方向に並べる

import SwiftUI

struct ContentView : View {
    var body: some View {
        VStack {
            Text("Hello World!!!!!")
                .font(.title)
                .color(.green)
            Text("Hello World!!!!!")
                .font(.subheadline)
        }
    }
}

f:id:qkuroneko:20190612165117p:plain:w320

  • VStackにalignmentつけつつ横方向に並べる
  • Spacer()は空いてるスペース埋めちゃう感じ
import SwiftUI

struct ContentView : View {
    var body: some View {
        VStack(alignment: .trailing) {
            Text("Hello World!!!!!")
                .font(.title)
                .color(.green)
            HStack {
                Text("Hello World!!!!!")
                    .font(.subheadline)
                Spacer()
                Text("foooo")
            }
        }
    }
}
  • padding
import SwiftUI

struct ContentView : View {
    var body: some View {
        VStack(alignment: .trailing) {
            Text("Hello World!!!!!")
                .font(.title)
                .color(.green)
            HStack {
                Text("Hello World!!!!!")
                    .font(.subheadline)
                Spacer()
                Text("foooo")
            }
        }
        .padding(.leading)
    }
}
  • .padding()だけだと全方向にpaddingできるのかな。

  • 画像を丸くする

  • ページ上部のリンクからサンプルをDLするとチュートリアルで使用している画像を手に入れることができます。

  • Assetsに画像をD&Dします
  • 新しいSwiftUIファイルを作って「CircleImage」って名前をつけます
CircleImage.swift

import SwiftUI

struct CircleImage : View {
    var body: some View {
        Image("turtlerock")
        .clipShape(Circle())
        .overlay(Circle().stroke(Color.white, lineWidth: 4))
        .shadow(radius: 10)
    }
}
  • .clipShape(Circle())で画像を丸くできる
  • .overlay()で画像の周りにstrokeをつけることができる
  • .shadow()で画像の周りに影をつけられる
  • RootのViewでCircleImage()を呼ぶと丸く切り取られた画像のViewを呼び出すことができる

f:id:qkuroneko:20190612175804p:plain:w320

  • 地図を使う
  • MapView.swiftファイルを作る
MapView.swift


import SwiftUI
import MapKit

struct MapView: UIViewRepresentable {
    func makeUIView(context: Context) -> MKMapView {
        MKMapView(frame: .zero)
    }
    
    func updateUIView(_ view: MKMapView, context: Context) {
        let coordinate = CLLocationCoordinate2D(
            latitude: 34.011286, longitude: -116.166868)
        let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0)
        let region = MKCoordinateRegion(center: coordinate, span: span)
        view.setRegion(region, animated: true)
    }
}
  • UIViewRepresentableを継承する
  • このクラスのことよく分からないのですがmakeUIView()updateUIView()をオーバーライドして地図の大きさやどこを指すかとか設定って感じ

  • これまで学んだことを組み合わせするとこんなのができる

f:id:qkuroneko:20190612180353p:plain:w320

import SwiftUI

struct ContentView : View {
    var body: some View {
        
        VStack {
            MapView()
                .frame(height: 300)
            
            CircleImage()
                .offset(y: -130)
                .padding(.bottom, -130)
            
            VStack(alignment: .leading) {
                Text("Turtle Rock")
                    .font(.title)
                HStack(alignment: .top) {
                    Text("Joshua Tree National Park")
                        .font(.subheadline)
                    Spacer()
                    Text("California")
                        .font(.subheadline)
                }
            }
            .padding()
            
            Spacer()
        }
    }
}
  • 真ん中に鎮座している画像の位置をoffsetで上のViewに乗せてるんですね。

  • チュートリアルを最後まで進めたら一番下に問題があるので答えてみます。

こんな感じでチュートリアルの「Creating and Combining Views」は終了です。
後日、続きをやっていきたいと思います。

【Web】PWAにしてみた

こんにちは。
先日、Flutter for webでwebアプリをリリースしたのですが、そのページをPWAにしてみました。

qkuronekop.hatenablog.jp

やり方は簡単。

manifest.jsonというファイルを作成します。

{
    "name": "qkuronekop.dev",
    "short_name": "qkuronekop",
    "background_color": "#ffffff",
    "icons": [{
        "src": "./icon-256.png",
        "sizes": "256x256",
        "type": "image/png"
      },{
        "src": "./icon-192.png",
        "sizes": "192x192",
        "type": "image/png"
      }],
    "start_url": "./?utm_source=homescreen",
    "display": "standalone"
  } 

name,short_name,background_color,icons,start_urlは自分のサイトに合わせて変更してください。

アイコンは最低、256×256と192×192の2種類のサイズが必要みたいです。

service-worker.jsというファイルを用意します。

self.addEventListener('install', function(e) {
    console.log('[ServiceWorker] Install');
  });
  
  self.addEventListener('activate', function(e) {
    console.log('[ServiceWorker] Activate');
  });
  
  self.addEventListener('fetch', function(event) {});

こんな感じ。

index.html

  <link rel="manifest" href="./manifest.json">
  <script>
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('./service-worker.js').then(function() { console.log('Service Worker Registered'); });
   }
  </script>

これを追加。

諸々パスに気をつけてくださいね。
私は全てindex.htmlがある階層においてます。

https://qkuronekop.dev/#/

PCブラウザで開いてもなんの変哲もないのですが、スマホで開くと

f:id:qkuroneko:20190607095636p:plain:w320

こんな感じになります。
ただし、アプリを追加しますか?と出てくるのは2回目以降の訪問の時で前回の訪問から5分以上経過している場合だそうです。

f:id:qkuroneko:20190607095904p:plain:w320

「追加」をタップすると、

f:id:qkuroneko:20190607100026p:plain:w320

こんな感じでホームにアイコンが追加されます。
アイコンをロングタップすると「アプリの情報」というモーダルが出てこのアイコンがただのブックマークではなくアプリであることがわかります。

f:id:qkuroneko:20190607100335p:plain:w320

さらに、ホームのアイコンからアプリを開くと上部にあったURLが消えていてネイティブアプリっぽい表示になります。

また、画像はないのですが、manifest.jsonにname、background_color、iconsを設定すると画面が開く直前にスプラッシュスクリーンを表示してくれるようになります。
qkuronekop.devもスプラッシュスクリーンが表示されているので、ぜひお手元のスマホでみてみてください。

iOSだとうまく表示されてないかもしれないです。

iOSでの対処法はこちらの方が紹介してくれていますのでご参考にどうぞ。

qiita.com

【Flutter】Flutter for Web使ってみた

こんにちは。
Google IO 2019にて発表されたFlutetr for webが発表されましたね。
どんなもんかと思ってさっそく使ってみました。

github.com

私が試した方法です。
上のリポジトリをCloneします。

hello_woldディレクトリをコピーしてディレクトリ名を変更します。

pubspec.ymlを以下のように書き換えます。

name: qkuronekop.dev

environment:
  # You must be using Flutter >=1.5.0 or Dart >=2.3.0
  sdk: '>=2.3.0-dev.0.1 <3.0.0'

dependencies:
  flutter_web: any
  flutter_web_ui: any

dev_dependencies:
  build_runner: ^1.4.0
  build_web_compilers: ^2.0.0

dependency_overrides:
  flutter_web:
    git:
      url: https://github.com/flutter/flutter_web
      path: packages/flutter_web
  flutter_web_ui:
    git:
      url: https://github.com/flutter/flutter_web
      path: packages/flutter_web_ui

サンプルのままだとflutter_webflutter_web_ui相対パスになっているので直しただけですね。

あと、nameのところを変更しました。

ここまでできればあとはlib/main.dartを自由に書き換えればOKですね。

動かし方はflutter_webのリポジトリのREADMEに詳しくあるのですが、flutter pub global activate webdevでwevdevをインストールしておきます。
先ほど作ったプロジェクトのルートでflutter pub upgradeします。
webdev serveしビルドに成功するとhttp://127.0.0.1:8080で接続できます。

開発中はwebdev serve --auto restartで起動するとホットリロードしてくれます。

せっかくなので、Firbase hostingでページを公開してみました。

flutter for webで作ったページをfirebaseで公開する方法はこちらの記事を参考にさせていただきました。

qiita.com

作ったページはこちら。

https://qkuronekop.dev/#/

すごく簡素な感じなのでおいおい色々試して行きたいです。 あと、スマホで見ると文字が切れてるかも。

main.dart

// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter_web/material.dart';
import 'dart:js' as js;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
            // This is the theme of your application.
            //
            // Try running your application with "flutter run". You'll see the
            // application has a blue toolbar. Then, without quitting the app, try
            // changing the primarySwatch below to Colors.green and then invoke
            // "hot reload" (press "r" in the console where you ran "flutter run",
            // or simply save your changes to "hot reload" in a Flutter IDE).
            // Notice that the counter didn't reset back to zero; the application
            // is not restarted.
            primarySwatch: Colors.deepPurple,
            primaryColorDark: Colors.deepPurpleAccent),
        home: Scaffold(
            appBar: AppBar(
              title: Text(
                'qkuronekop.dev',
                textDirection: TextDirection.ltr,
              ),
            ),
            body: SingleChildScrollView(
              child: Stack(
                children: <Widget>[
                  Column(
                    children: <Widget>[
                      Container(
                        margin: EdgeInsets.all(16.0),
                        child: Column(
                          children: <Widget>[
                            SizedBox(
                              height: 16.0,
                            ),
                            Center(
                              child: Text(
                                'Profile',
                                textDirection: TextDirection.ltr,
                                style: TextStyle(
                                    fontSize: 28.0,
                                    fontWeight: FontWeight.bold),
                              ),
                            ),
                            SizedBox(
                              height: 24.0,
                            ),
                            Center(
                              child: Row(
                                mainAxisAlignment: MainAxisAlignment.center,
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: <Widget>[
                                  Container(
                                    height: 48.0,
                                    width: 48.0,
                                    decoration: BoxDecoration(
                                      image: DecorationImage(
                                          image: NetworkImage(
                                              '画像URL'),
                                          fit: BoxFit.fill),
                                      borderRadius: BorderRadius.circular(24.0),
                                      boxShadow: [
                                        BoxShadow(
                                            color: Colors.black38,
                                            offset: Offset(2.0, 2.0))
                                      ],
                                    ),
                                  ),
                                  SizedBox(
                                    width: 16.0,
                                  ),
                                  Container(
                                    width: 400.0,
                                    child: Column(
                                      crossAxisAlignment:
                                          CrossAxisAlignment.start,
                                      children: <Widget>[
                                        Text('こんにちは。\n辻村里美(@qkuronekop)と申します。'),
                                        Text(
                                            'Androidアプリ開発が得意です。\n現在は、KotlinでのAndroid開発やFlutterでのAndroid、iOS開発をしています。'),
                                        Text('上記以外に興味があるのはGo言語でのWebアプリ開発です。'),
                                      ],
                                    ),
                                  )
                                ],
                              ),
                            ),
                            Container(
                              margin: EdgeInsets.symmetric(vertical: 32.0),
                              width: 300.0,
                              height: 2.0,
                              color: Colors.purpleAccent,
                            ),
                            Center(
                              child: Text(
                                'Github',
                                textDirection: TextDirection.ltr,
                                style: TextStyle(
                                    fontSize: 28.0,
                                    fontWeight: FontWeight.bold),
                              ),
                            ),
                            SizedBox(
                              height: 24.0,
                            ),
                            GestureDetector(
                              child: Text(
                                'https://github.com/qkuronekop',
                                style: TextStyle(
                                  color: Colors.deepPurpleAccent,
                                  decoration: TextDecoration.underline,
                                  decorationColor: Colors.deepPurpleAccent,
                                  decorationStyle: TextDecorationStyle.solid,
                                ),
                              ),
                              onTap: () {
                                js.context.callMethod(
                                    "open", ["https://github.com/qkuronekop"]);
                              },
                            ),
                            Container(
                              margin: EdgeInsets.symmetric(vertical: 32.0),
                              width: 300.0,
                              height: 2.0,
                              color: Colors.purpleAccent,
                            ),
                            Center(
                              child: Text(
                                'Blog',
                                textDirection: TextDirection.ltr,
                                style: TextStyle(
                                    fontSize: 28.0,
                                    fontWeight: FontWeight.bold),
                              ),
                            ),
                            SizedBox(
                              height: 24.0,
                            ),
                            GestureDetector(
                              child: Text('http://qkuronekop.hatenablog.jp/',
                                  style: TextStyle(
                                    color: Colors.deepPurpleAccent,
                                    decoration: TextDecoration.underline,
                                    decorationColor: Colors.deepPurpleAccent,
                                    decorationStyle: TextDecorationStyle.solid,
                                  )),
                              onTap: () {
                                js.context.callMethod("open",
                                    ["http://qkuronekop.hatenablog.jp/"]);
                              },
                            ),
                            Container(
                              margin: EdgeInsets.symmetric(vertical: 32.0),
                              width: 300.0,
                              height: 2.0,
                              color: Colors.purpleAccent,
                            ),
                            SizedBox(
                              height: 24.0,
                            ),
                            Center(
                              child: Text(
                                'App',
                                textDirection: TextDirection.ltr,
                                style: TextStyle(
                                    fontSize: 28.0,
                                    fontWeight: FontWeight.bold),
                              ),
                            ),
                            SizedBox(
                              height: 24.0,
                            ),
                            GestureDetector(
                              child: Container(
                                width: 320.0,
                                child: Image.network(
                                    '画像URL'),
                              ),
                              onTap: () {
                                js.context.callMethod("open", [
                                  "https://play.google.com/store/apps/developer?id=qkuronekop&hl=ja"
                                ]);
                              },
                            )
                          ],
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            )));
  }
}

現状感じていること

  • 外部リンクができないのでdart:jsで外部リンクを開くスクリプトを書かねばならない。リンクではなくonTapを使っているのでPCでみた時にカーソルが変化しないのがちょっと微妙かな。
  • 当たり前だけど今あるNativeアプリに依存するようなpackageは使えないのでweb用のライブラリとかが充実してくるといいな。
  • Image.assetが使えないんだけど私だけ???
  • CSS書かなくてもいい感じのUIが作れるのは最高。   

2019/6/26追記

Image.assetについて勘違いしていたことがあって、Nativeの時みたいにpubspec.ymlにパスかいて使うのではなくweb/assets/にリソースを置いて、そこのパスを書けばいいんですね。
しかし、ローカルでは表示されるけどfirebase hostingにアップすると表示されなくなる……なぜだ。。。

ちょっと色々デザインいじってみました。 gitにも公開したので興味ある方はみてください!

github.com

【bot】Cogbot作ってきました

cogbot.connpass.com

こんにちは。
先日、こんな勉強会へ参加してきました。

AzureとLine Message Apiを使ってラインbotを作るハンズオンです。

私は「食べれるかニャン?BOT」を作りました。

f:id:qkuroneko:20190422174245p:plain

「食べれるかニャン?BOT」は食べ物の名前を送信すると、猫が食べられるかどうかを教えてくれるbotです。
作り方は超簡単で、ノーコーディングで1時間くらいで作れました。

興味がある方は上のコンパスのリンクからページを開くと、作り方を詳細に記載したqiitaのリンクが貼ってあります。
AzureとLineのアカウントとパソコンがあればすぐに作れます。

「食べれるかニャン?BOT」でも使ったのですが、QnA Maker を使うと、サービスのQ&Aみたいに簡単な応答のbotならすぐ作れちゃいます。
便利ですね。

もっと複雑なことがしたければ、ノーコーディングでは難しそうですが、Cloud functionなどと組み合わせると色々できそうな感じでしたね。

とりあえず作ったみたいだけなら全て無料でできますので、色々作ってみると楽しそうですね。

【Flutter】Firebase Storageに画像を保存する

こんにちは。
前回、写真を撮る機能をアプリに追加したのですが。

qkuronekop.hatenablog.jp

この撮った写真をFirebase Storageに保存したいと思います。

pubspec.yml

dependencies:
  flutter:
    sdk: flutter
  firebase_storage: 1.0.4

AndroidX関係でエラーになるので、下記のリンク先のバージョンに合わせます。

flutter.dev

import

import 'package:firebase_storage/firebase_storage.dart';

code

  Future<Null> uploadFile(String filePath) async {
    final ByteData bytes = await rootBundle.load(filePath);
    final Directory tempDir = Directory.systemTemp;
    final String fileName = "${Random().nextInt(10000)}.jpg";
    final File file = File('${tempDir.path}/$fileName');
    file.writeAsBytes(bytes.buffer.asInt8List(), mode: FileMode.write);

    final StorageReference ref = FirebaseStorage.instance.ref().child(fileName);
    final StorageUploadTask task = ref.putFile(file);
  }

こんな感じでファイルを作り、Firebase Strageにuploadします。

 RaisedButton(
   color: Colors.grey,
   shape: RoundedRectangleBorder(
     borderRadius: BorderRadius.all(Radius.circular(10.0)),
   ),
   child: Icon(Icons.camera),
   onPressed: () async {
     var filePath = await takePicture();
     if (mounted) {
       setState(() {
         imagePath = filePath;
       });
     }
     await uploadFile(filePath);
   },
 ),

前回作ったカメラ画面のシャッターボタンのonPressed()で、画像のパス取得後にuploadFile()を呼び出しています。

私のアプリの場合には、匿名ログインの機能を入れいているので、この状態で動作させることが可能です。

qkuronekop.hatenablog.jp

が、FirebaseAuthを入れていない場合にはデフォルトでwrite権限がないのでFirebase consoleのStorageメニューを選択し、ルールタブを開きアクセス権の変更をします。

f:id:qkuroneko:20190323074416p:plain

service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write;
    }
  }
}

ルールをこんな感じに変更します。
これでトークンがなくてもread、writeすることができます。
ちょっと機能を試してみたい場合にはこれでいいかと思います。

こちらを参考にしました。

www.youtube.com

【Flutter】写真撮影をする

Flutterで写真を撮るアプリを作りたいと思ったのでカメラ機能を入れてみました。

使ったプラグインはこちら。

pub.dartlang.org

まだAndroidでしか試してないけどもiOS/Androidで使えます。
Androidの場合minSdkVersion 21です。

使い方

pubspec.yml

dependencies:
  flutter:
    sdk: flutter
  camera: ^0.4.2

これを追加してPackages getします。

camera.dart

カメラ機能を追加する画面をcamera.dartとして作ります。

class CameraView extends StatefulWidget {

  @override
  State createState() => CameraState();

}

class CameraState extends State<CameraView> {
 @override
  Widget build(BuildContext context) {
  }
}

まずはStatefulWidgetで画面を作ります。

class CameraState extends State<CameraView> {

  CameraController controller;

  Future<void> getCameras() async {
    cameras = await availableCameras();
    controller = CameraController(cameras[0], ResolutionPreset.medium);
  }

  @override
  void initState() {
    super.initState();
    getCameras().then((_) {
      controller.initialize().then((_) {
        if (!mounted) {
          return;
        }
        setState(() {});
      });
    });
  }
}

initState()で使えるカメラを取得して、カメラコントローラーの初期化をします。
私が作りたいアプリの場合背面カメラの静止画だけでいいかなと思ったので1種類だけ使ってます。
機種に乗っていればフロントカメラやムービー撮影も使えるようです。

class CameraState extends State<CameraView> {
 @override
  Widget build(BuildContext context) {
    if (controller == null || !controller.value.isInitialized) {
      return Container();
    } else {
    }
  }
}

画面が起動した直後、またはカメラへのアクセス許可がされていない状態ではカメラプレビューを表示できないので、
コントローラーがnullまたはisInitializedfalseの場合にはカメラプレビューではないWidgetを返します。

class CameraState extends State<CameraView> {
 @override
  Widget build(BuildContext context) {
    if (controller == null || !controller.value.isInitialized) {
      return Container();
    } else {
      return AspectRatio(
                  aspectRatio: controller.value.aspectRatio,
                  child: CameraPreview(controller)
                 );
    }
  }
}

AspectRatioは縦横比を維持したまま、最大の大きさに広がってくれるWidgetです。
カメラcontrollerが縦横比を持っているのでそれを設定してあげるといい感じの大きさのプレビューが作れます。

CameraPreview(controller)のところが実際のカメラプレビューになります。

class CameraState extends State<CameraView> {

  Future<String> takePicture() async {
    final Directory extDir = await getApplicationDocumentsDirectory();
    final String dirPath = '${extDir.path}/Pictures/own_note';
    await Directory(dirPath).create(recursive: true);
    final String filePath = '$dirPath/${timestamp()}.jpg';

    if (controller.value.isTakingPicture) {
      // A capture is already pending, do nothing.
      return null;
    }

    try {
      await controller.takePicture(filePath);
    } on CameraException catch (e) {
      // エラー時の処理
      return null;
    }
    return filePath;
  }

 @override
  Widget build(BuildContext context) {
    if (controller == null || !controller.value.isInitialized) {
      return Container();
    } else {
      return Column(
            children: <Widget>[
               AspectRatio(
                  aspectRatio: controller.value.aspectRatio,
                  child: CameraPreview(controller)),
               RaisedButton(
                  child: Icon(Icons.camera),
                  onPressed: () async {
                      var filePath = await takePicture();
                  },
               ),
            ]
      );
    }
  }
}

シャッターボタンを追加します。
ボタンWidgetonPressed()takePicture()を呼び出します。
takePicture()では、保存先のパスとファイル名を作り、画像を保存しています。

私が作りたいアプリの場合には、写真を撮った直後に同じ画面内にサムネイルを表示しつつ、
画像をサーバーへアップロードしているのでtakePicture()が成功した場合にはファイルパスを返すようにしました。

f:id:qkuroneko:20190323071353p:plain:w320

色々装飾してますが、こんな感じになります。

ここを参考にしました。

github.com