20200725のAndroidに関する記事は8件です。

sora-unity-sdkがAndroidに正式対応したらしいのでサンプルを動かしてみた

a.gif

株式会社時雨堂様が提供してくださっているsora-unity-sdkがAndroidに正式に対応したらしいので、サンプルを動かしてみました。

試した環境

sora-unity-sdk 2020.5

PC
Windows10
Unity2019.1.14f1

スマホ
Android10
Motog8

1.サンプルの入手

GitHubから入手。
https://github.com/shiguredo/sora-unity-sdk-samples

READMEに依ると

sora-unity-sdk のインストール
Windows の場合は install.bat を、macOS の場合は install.sh を実行して下さい。 これで各種サンプルを実行するために必要になる sora-unity-sdk をインストールできます。

とのことなのでinstall.batを実行します。しかしこの実行ファイルの中身を見てみるとsora-unity-sdk v2020.4をインストールする設定になっているようです。今回、試したいのは2020.5だったのでinstall.ps1の$SORAUNITYSDK_VERSIONを修正します。

install.ps1
$SORAUNITYSDK_VERSION = "2020.5"

修正した上でコマンドプロンプトでinstall.batを実行して、sora-unity-sdk v2020.5をプロジェクトにインストールします。

mojikyo45_640-2.gif

2.Sora Laboを使うように設定

とりあえず動かしてみたいので、時雨堂様が提供してくださっているSora Laboを利用します。検証目的なら無償で利用できますが、商用利用はできないようです。簡単な利用規約があるのでよく確認してから利用しましょう。今回は検証目的なので大丈夫です。
https://sora-labo.shiguredo.jp/
mojikyo45_640-2.gif

サインアップしてログインしたらUnityに戻って設定していきます。

適当なサンプルシーンを開きます。今回はmulti_sendrecvにします。
mojikyo45_640-2.gif

ScriptというオブジェクトにアタッチされているSora SampleコンポーネントにSora Laboに接続するための設定を記入していきます。

Sora Laboのダッシュボードに記述してあるシグナリングURL, シグナリングキーをコピペしてUnityのSora Sampleコンポーネントに貼り付けます。

mojikyo45_640-2.gif

次にChannel Idを設定します。sora-laboのドキュメントに依ると

チャネル ID を <自分の GitHub Username>@<好きな Room ID> のように指定してください

とのことなので適当に設定します。ここでは便宜上<GITHUB_USERNAME>@sora-laboとします。

また、今回はUnityのカメラを送受信したいので、Capture Unity Cameraのチェックを入れます。

mojikyo45_640-2.gif

ここまでの設定でUnity Editor上で挙動を確認できるようになったはずです。Sora Laboにマルチストリーム受信というページがあるので、そこから確認してみます。Unityカメラの映像がブラウザから確認できるはずです。

mojikyo45_640-2.gif

3.Androidで確認できるように設定

Android向けにビルドして実機で確認できるように設定していきます。まずビルドターゲットをAndroidに変更します。

sora-unity-sdkのドキュメントによると

  • arm64-v8a のライブラリしか入れていないので armeabi-v7a の端末では動きません。
    • libSoraUnitySdk.so のインスペクタ -> Platform settings -> Android の設定で CPU が ARM64 になっていることを確認して下さい。
    • Player Settings -> Other Settings -> Target Architectures で ARM64 にチェックが入っていることを確認して下さい。
  • Vulkan で動かす必要があります。
    • Player Settings -> Other Settings の Graphics APIs で Vulkan が先頭にあることを確認して下さい。
  • OpenGLES モードでの動作は未確認です。
  • 最低でも API LEVEL 24 (Android 7.0)が必要です。
  • Pixel 3 で解像度が 16 の倍数じゃない時に映像が乱れる問題があります。

とのことなので、書いてある通りに設定していきます。

まずAssets\Plugins\SoraUnitySdk\android\arm64-v8a\libSoraUnitySdk.soのTarget ArchitecturesをARM64に変更します。変更したら忘れずにApplyしましょう。

mojikyo45_640-2.gif

次にPlayer Settingsの設定を変更していきます。Other SettingsのTarget ArchitecturesをARM64に。Graphics APIsのAuto Graphics APIのチェックをはずしてVulkanに設定します。さらにMinimum API LevelをAPI LEVEL 24にしておきましょう。

mojikyo45_640-2.gif

4.オブジェクトが映らない問題

一応、以上で設定はできたはずなんですが、私の環境だと問題が発生しました。Unityのカメラにオブジェクトが映らないという問題です。

mojikyo45_640-2.gif

本来なら球体が映らはずだけど、映らない。この問題はAndroidの実機だけじゃなくてUnity Editor上でも発生しました。

結論から言うと、Project Settings->Graphics->Tier SettingsのUser DafaultsのチェックをはずしてUSE HDRのチェックを入れると解決します。

mojikyo45_640-2.gif

この問題が起こった理由は特定できていませんが、検証してみた結果、ShaderのRenderTargetがOpaqueのオブジェクトが映らないという症状のようです。RenderTargetをTransparentにすると映るようになりました。まだBuild Settingsで対象プラットフォームをPCにしても治るようです。たぶんVulkan関連の何かが原因な気がします。

何か必要な設定を忘れていたのか、自分の実行環境の問題なのか、よく分かりません。

なんか描画順とか、Unityのカメラをキャプチャしてテクスチャにする処理とかに何かヒントがある気がします。WaitForEndOfFrameのタイミングでUnityのカメラの映像をVulkanを直接叩いてキャプチャするっていう実装になっているようなんですが、これでなんで映らないのか謎です。WaitForEndOfFrameは描画系の処理の最後に実行されるようなので、キャプチャするタイミングとしては適切な気がします。

原因を探るならたぶんこの辺のキャプチャする仕組みがどうなってるのか見る必要がある気がするんですが、Vulkanとか低レイヤーのことは門外漢なので、私にはよく分からなかったです。今回はとりあえずUSE HDRのチェックを入れたら写ったので良しとしました。
https://github.com/shiguredo/sora-unity-sdk/blob/develop/src/unity_camera_capturer_vulkan.cpp

5.結果

ということでsora-unity-sdkをAndroidで使うことができました。

a.gif

使っているスマホはMotog8というローエンド端末なんですが、想像以上にヌルヌル動きます。遅延もほとんどないと言っていいんじゃないでしょうか。すごいですね。

6.最後に

先日、NDI SDKをAndroidで使うっていう記事を書きました。NDIはローカルネットワーク上で映像や音声の送信を行うのに対してsora-unity-sdkはSoraを介してどこからでもアクセスできます。

またNDI SDKのAndroid実装は送信にしか対応してませんでしたが、こちらは送信も受信もできます。ただSoraを商用利用するにはお金がかかります。(NDI SDKは無料だったはず)

NDIもWebRTCも使うなら、表面的な理解だけでは駄目で、チューニングして軽量化したりする必要がありますね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactNative+ExpoでAndroidアプリをビルドする

ReactNative+ExpoでAndroidアプリをビルドする場合にいくつか面倒なことがあったので、備忘録を残します。

ReactNativeとは

1280px-React-icon.svg.png

ReactNativeはFacebookが開発したモバイルアプリケーションフレークワークです。
Reactの機能をネイティブアプリに使うことができ、マルチプラットフォーム対応で、JavascriptでiOSアプリとAndroidアプリを作ることができます。

Javascriptを使うモバイルアプリケーションフレークワークはMonacaやCORDOVAなどいくつかありますが、ReactNativeの私的なメリット・デメリットは下記です。

メリット

・ネイティブアプリ並の速さが出る
 ブリッジモジュールを介して、OSのネイティブな機能を使っているので速い(らしい)
 ただ、ハードウェア性能と該当OSのJavaScriptインタプリタの性能に依存する(らしい)ので、端末依存の面も大きい
・独自のネイティブモジュールを実装することができる
 ※カメラや通知などを行うネイティブモジュールはデフォルトで揃っています
・ReactやReact ReduxなどのReactの機能を使うことができ、React開発者は学習コストが少なくすむ

デメリット

・AndroidJava+AndroidStudioやSwift+Xcodeを使ったアプリ開発よりも自由度が低い
 これは他のフレークワークも当てはまりますが、色々とビルドが面倒だったり、最新のAndroid/iOSに対応していなかったりします。
・独自のネイティブモジュールを作るには、AndroidJava/Swiftの知識が必要

といったところです。大規模なアプリの開発なら、素直にAndroidJava+AndroidStudioとSwift+Xcodeを使った方がいいです。

Expo

unnamed.png

Expoは、ReactNativeの開発支援ツールです。
ReactNative+Expoによって、ビルドやエミュレータ・実機での確認が楽になります。

Expoにもデメリットはありますが、ここは以前Expoを使った開発について書いた記事がありますので、
読んでいただけますとありがたいです。
他者様の記事も参考にしてみてください。

前置きはここまで、ここからAndroidアプリのビルドに入ります。

Androidアプリのビルド

ReactNative+Expoのビルドの設定は、app.jsonで行います。
AndroidManifest.xmlのようなものです。

app.json
{
  "expo": {
    "name": ${アプリ名},
    "slug": ${アプリ名},
    "platforms": [
      "ios",
      "android",
      "web"
    ],
    "sdkVersion": "36.0.0",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "updates": {
      "fallbackToCacheTimeout": 0
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "ios": {
        "supportsTablet": true,
        "bundleIdentifier": ${開発者ドメイン}
    },
    "android": {
        "package": ${開発者ドメイン},
        "versionCode": 4,
        "permissions": []
    }
  }
}

カスタムした点だけ書きます。

"sdkVersion"

ReactNativeのSdkVersionを指定します。2020年7月現在、ここを指定しないと、AndroidSdkVersionが28に指定されるため、2020年8月以降、アプリ非対応になります。
なお、expoのバージョンが古いと、そもそもAndroidSdkVersion29以上に対応できません。
expoやreact-nativeの更新は、

$ expo upgrade

で行えます。
参考に動作したアプリのバージョンを書きます。

package.json
"dependencies": {
  "expo": "^37.0.0",
  "react": "16.9.0",
  "react-dom": "16.9.0",
  "react-native": "https://github.com/expo/react-native/archive/sdk-37.0.1.tar.gz",
  "react-native-web": "^0.11.7",
},

"versionCode"

Androidアプリのリリースでは、同じバージョンコードのファイルをアップすることはできません。
VersionCodeを指定すると対応できます。

"permissions"

この項目を指定しないでビルドすると、カメラやストレージを使用する設定になっています。そのため、使用しない設定にする必要があります。空カッコ([])で使用しない設定になります。
使用する場合は下記のように書きます。

"permissions": [
  "ACCESS_COARSE_LOCATION",
  "ACCESS_FINE_LOCATION",
  "CAMERA",
  "READ_EXTERNAL_STORAGE",
  "WRITE_EXTERNAL_STORAGE",
  "VIBRATE",
]

ビルド

ApkファイルとAndroid App Bundleファイルは下記のコマンドで作れます。

ビルドでApkファイルを作成

$ expo build:android

ビルドでAndroid App Bundleファイル(最適化している)を作成

$ expo build:android --type app-bundle

build.png

特に理由がなければ、Android App Bundleファイルを作りましょう。
作ったファイルをアップロードしてリリース完了です。

Expoは便利ですね〜。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

大学生が1週間でFlutterアプリを学んでリリースした過程(7日目)

こんにちはシオンです。

ついに最終日がきました。
ここまで読んでくださって本当にありがとうございます。
思いつきで始めたこの企画ですが、いい感じにFlutterの勉強になったし、記事を書く楽しさも感じられて始めてすごく良かったなと感じています。
ですが、今日アプリが完成しないことには終わるに終われませんので、引き続きラストスパート頑張りたいと思います。

目次

■完成までにやること
■路線変更:AppStoreへアップする
■いよいよ審査!
■まとめ

■完成までにやること

今日アプリを完成させるためにやらなければいけないことを先にまとめておきます。
大きくやることを分けると
①アプリのコードを書ききること。
②リリース申請をすること。
この2つになります。

では、それぞれ進めていきます。

①アプリのコードを書ききる

現状画面を全て作成し終わって、画面遷移まで実装できました。
残る機能は以下の通りです。
1. 2画面目でストレスLvを表示
2. 3画面目に2画面目で入力した愚痴タイトルを表示
3. 乗り越えた数のカウントと保存
まず、これらを実装してアプリを完成させます。上から順にやっていきましょう。

1.1 ストレスLvを表示する

ストレスLvを表示するためには、5つのボタンをタップした時にタップしたボタンに応じて炎の画像とLvを更新して表示すれば良さそうです。
例えば5つの丸ボタンの真ん中のボタンを押すと左から3つが炎の画像が現れて、Lvが3になる。
といったかんじです。

これを実装するための手順として考えられるのは
・予め画像をボタンに重ねて配置し、表示をしない設定にしておく。
・ボタンを押されると、画像が現れる+数字の表示が変わる
という手順でやってみます。
なのでそのために、ボタンと画像を重ねて配置する方法と表示をしない設定のやり方、テキストの動的な変更の仕方を学んでいきます。

まず、ボタンと画像を重ねて表示するためにはウィジェットを重ねる方法がわかれば良さそうです。
では、ググってみましょう。
ググってみるとやはりあるんですね。やり方が(先人の方々本当にありがとうございます。)

どうやらStackというウィジェットの中に配置すると配置されたウィジェットは重なっていくらしいです。便利。
試しに全部炎を重ねて出してみます。

1.png

いい感じですね。

では次に、配置したウィジェットを非表示にする方法を調べましょう。
同じようにググります。
調べるとやはりあるんですね!本当に感謝。

やり方は2通りあるそう。一つは予想通りのやつでVisibleというウィジェットに画像を入れてやると表示非表示を切り替えられるからそれでやるパターン。もう一つは条件分岐を作って画像を画面を構成しているウィジェット の中に入れない状態と入れる状態を分ける。
見た感じ今回の実装では条件分岐のほうが適していそうなのでそっちでやってみたいと思います。

では、これでコードを一旦書いてみます。
2.png

表示非表示はいけました。
あとはこれをボタンから自由にできるようにしていきます。また、そのボタンを押した時にレベルも表示されるようにします。

3.png

よしっと。これでレベルの表示は完了ですね。次に進みます。

1.2 愚痴タイトルを3画面目に表示

これをやる前に内容入力のフォームを直しておきます。
今はタイトルと同じくなってしまっているので、これを大きくしたいです。
調べてみると、フォームの(テキストボックスというそう)の限界行数が1になってしまっているよう。これを制限なし(null)にしてあげれば良さそうです。

4.png

よしよし、では本題のタイトルを渡す作業をしていきましょう。
これをやるには画面遷移時に値を渡す方法がわかればすぐにできそうなので、学んでいきます。

画面遷移時の値渡しは初めてアプリを作った時に結構つまづいた経験があるのでしっかりみていきまましょう。

と、息巻いて調べたんですがすごく単純で前回画面遷移を実装したところで変数を渡せば良いそう。できましたね。
受け取り側と、渡す側にそれぞれ変数を置いて渡す側の変数にタイトルの文字を入れてやればOKです。

そうすると

5.png

このようになりました。これで完了ですね!
(ちなみにタイトルは事実です。)

1.3 乗り越えた数のカウントと保存

いや、本当に申し訳ないと思うのですが今回この機能は未実装でいきたいと思います。
自分の力不足で申し訳ないですが、リリース準備を考えると時間が全然足りないことに気づいてしまったため今回は見送りたいと思います。

もしもこの先万が一にでもこのアプリをアップデートしたいと思うことがあれば追加します。

なので1画面目と2画面目の現在乗り越えた数を削除して、配色を調整してアプリを完成にしたいと思います!
(申し訳程度に、タイトルを入力しないとボタンを押せない処理だけ追加しておきました。。)

②リリース申請をする

リリースですが、今日のところはGooglePlayStoreでやってみたいと思います。理由はやったことがないからです。
今回この企画は勉強することがいちばんの目的なのでやったことがないことを基本的にやっていきたいと思います。

この企画の2日目の記事によるとリリースするためには
1. Googleディベロッパーアカウントを作成する
2. デジタル署名したパッケージファイル(apkファイル)を準備する
3. GooglePlayにアプリケーションを登録する
この三つが必要になるそうです。上から順に用意していきましょう。

2.1 アカウント登録する

これはすぐできそうですね。調べてみます。

調べた結果ですが、25ドルお支払いすれば登録はすぐに終わるらしいです。
早速登録してみましょう。

6.png

・・・・・・
登録していちばん上に出てきたのですが、なんと審査には一週間以上かかるようです。。。
審査に時間がかかることはわかっていました。わかっていましたが想像以上でした。(でもこのご時世ですから仕方ないですね。Google様いつもありがとうございます。)

ですが、流石に一週間は待てないので今回はAppStoreに先に申請出したいと思います。それで審査が通ればGooglePlayStoreにも後ほどアップしたいと思います。

■路線変更:AppStoreへアップする

AppStoreへのアップ方法は大体GooglePlayStoreと一緒ですね。
登録はもう済んでるのでアプリをビルドしたて必要なスクリーンショット などを用意して提出。という感じになります。

ただ、Flutterでリリース用のビルドをしたことがないのでやり方を調べてみると、どうやらアプリのアイコンを作ってアプリの中に入れておく必要があるようですね。

なるほど、というわけで作ってきました。

7.png

どうですか。やはり世の中「シンプル・イズ・ザベスト」ですね。
ここまでみてきた方なら私のデザイン力のなさにはそろそろ勘付いているかと思いますので、是非大目に見てくださると幸いです。

では、アイコンの用意ができましたのでビルドしてみたいと思います。
ビルドには下記のコマンドをターミナル上で実行すれば良いようです。

% flutter build ios

入力して、実行します。すると、、、エラーがでました。
「flutterなんてコマンド知らないよ!」と怒られました。知っとけよ!

これを解消するためには、ターミナルでなんやかんやする必要があるそうです。ざっくりいうとFlutterはパソコンの中にあるけどどこにあるか分からんからしっかり道を教えてくれ!という要求らしいのでこれを解消してみます。

解消にはvim(これまた初耳)という「エディター」を使わなければならないそう。(まずエディターが分からん。調べたらプログラムのことだそう。じゃあそういえよ。)

で、このvimのコマンドを調べてなんやかんやするとflutterコマンドが無事できるようになりました。

■いよいよ審査!

ビルドが完了したのでAppStoreにアップロードします。
アップロードの流れとしては、アプリの情報とかをAppleDeveloper(開発者用サイト)で用意、アプリをXcodeというツールでビルド、アプリをアップロード
こんな感じです。
(ここまでやって思いましたけど、多分さっきのvim使ってビルドする必要なかったですね。。。)

ここまでくればもうゴールは目の前です!
スクリーンショットAppStoreに表示されるスクリーンショット とか、紹介用の文章を書けば終了です!

まずスクリーンショット。
提出サイズに気をつけながら最低3枚提出します。

8.png

こんな感じに。
ちなみにこのスクショも簡単に無料で作れます。(今回はAppLaunchPadを使いました。)

そして残りの面倒な文章やら、プライバシーポリシーやらを片付けていき・・・

ついに・・・

審査に提出しました!!!

あとはAppleからのお返事を待つのみです。。

■まとめ

こんな記事をここまで読んでいただき本当にありがとうございます。
qiitaは技術的に参考になるような記事を各場所なのにブログのような投稿を続けてしまいました。今後投稿することがあればより技術的に有益な記事を投稿していきたいと思います。

また、この1週間Flutterに触れてみての感想ですが個人的にはすごく使いやすいと感じました。
僕はSwiftでもアプリを作ったことがあるのですが、Flutterの方がやや書きやすいかなーというのが体感です。
あとはなんと言ってもIphoneとAndroid両方のアプリを同時に作れるところですね。
まだほんの少ししか触っていないのでわかっていないことも多いですが、次に何かアプリを作るときにはFlutterを使いたいと思います。

最後の方はかなり端折って書いてしまいましたが、どなたかの参考になれば幸いです。
質問などありましたら、可能な限りお答えしますのでTwitterでもコメントでもお気軽にお願いします。

それでは、お疲れ様でした!(焼肉行きます。)

※審査に提出したアプリは無事一回でリリースすることができました。
もし使ってみたい方は下記からDLできます。(本当にクソアプリですので、触ったらすぐにアンインストールすることをお勧めします。)
https://apps.apple.com/us/app/id1516432646

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

有名人と対戦してる気分が味わえるオセロアプリ作ってみた

対戦相手の名前を入力するとランダムで画像を持ってくるオセロアプリを作りました。
ダウンロード (3.png ⇒ ダウンロード (3).png

名前を入力するとgoogleの検索結果から画像を取得してきます。
これを使えば憧れの「石原さとみ」さんや「綾瀬はるか」さんとオセロで対戦気分を味わえる!
カワイイ!ヤッター!

遊んでみた感想

こみ上げる虚無感。
作り始めた時は素晴らしいアイデアだと思っていたのですが。

使用したもの

Google Custom Search API
遅延処理(Handler)
非同期処理(AsyncTask)

苦労したところ

・オセロの裏返しロジック。
できたと思ってもバグが沢山見つかって大変でした。

・CPU戦の実装
地味なところだけど、黒番(人間)が打ってから白番(CPU)が打つまでに少し間を空けるのに1番苦労しました。Handlerを使って何とか実装。

良かったところ

・HashMapを初めて使えた。
・APIに触れることができた。
・Androidはアニメーションが比較的簡単に作れる。

反省点

・設計の重要性を感じた
設計をしないで作り始めたため、途中で何度も修正が入ってしまった。
 
・コードが汚い
オセロ盤のマス(8×8=64)が全てImageViewでできていて、全てにidを付けているためコードが大変なことに。
さらに1マスにつきImageViewを2枚重ねている。
つまりImageViewが128枚ある!
恐ろしい。
他に何か良い方法があったら教えてください。

参考サイト

・Google Custom Search APIについて
Custom Search APIを使ってGoogle検索結果を取得する
Google Custom Search APIを使って画像収集

・遅延処理
Androidにおける遅延実行の実装方法

・非同期処理
[Android] 非同期処理 AsyncTaskの使い方

ソースコード

Google Custom Search APIのとこだけ。

CustomSearch.java
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class CustomSearch extends AsyncTask<String, Bitmap, Bitmap> {
    String key = "APIキー";
    String cx = "検索エンジン ID";
    String link = null;
    String path = null;
    List<String> linkList = new ArrayList<>();
    private Listener listener;

    @Override
    protected Bitmap doInBackground(String... qry) {
        //URLを取得
        getLink(qry[0]);

        //10秒に1回画像を切り替える(9回まで)
        for(int i = 0; i < 9; i++) {
            try {
                publishProgress(downloadImage(qry[0]));
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        return downloadImage(qry[0]);
    }

    // 途中経過をメインスレッドに返す
    @Override
    protected void onProgressUpdate(Bitmap... bmp) {
        if (listener != null) {
            listener.onSuccess(bmp[0]);
        }
    }

    // 非同期処理が終了後、結果をメインスレッドに返す
    @Override
    protected void onPostExecute(Bitmap bmp) {
        if (listener != null) {
            listener.onSuccess(bmp);
        }
    }

//APIにリクエストを投げてレスポンスを取得する
    List<String> getLink(String qry) {
        try {
            URL url = new URL(
                    "https://www.googleapis.com/customsearch/v1?key=" + key + "&cx=" + cx + "&q=" + qry + "&searchType=image");
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setRequestProperty("Accept", "application/json");
            BufferedReader br = new BufferedReader(new InputStreamReader(
                    (conn.getInputStream())));

    //レスポンスはjson
    //jsonからURLを抜き出す処理
            String output;
            System.out.println("Output from Server .... \n");
            while ((output = br.readLine()) != null) {

                if (output.contains("\"link\": \"")) {

                    link = output.substring(output.indexOf("\"link\": \"") + ("\"link\": \"").length(), output.indexOf("\","));
                    System.out.println(link);
                    linkList.add(link);
                }
            }
            conn.disconnect();
            return linkList;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    //URLから画像をダウンロードする処理
    private Bitmap downloadImage(String address) {
        Bitmap bmp = null;
        HttpURLConnection urlConnection = null;
        Random random = new Random();

        try {
            // linkListの中からランダムでURLを選択する
            URL url = new URL(linkList.get(random.nextInt(10)));

            // HttpURLConnection インスタンス生成
            urlConnection = (HttpURLConnection) url.openConnection();

            // タイムアウト設定
            urlConnection.setReadTimeout(10000);
            urlConnection.setConnectTimeout(20000);

            // リクエストメソッド
            urlConnection.setRequestMethod("GET");

            // リダイレクトを自動で許可しない設定
            urlConnection.setInstanceFollowRedirects(false);

            // ヘッダーの設定(複数設定可能)
            urlConnection.setRequestProperty("Accept-Language", "jp");

            // 接続
            urlConnection.connect();

            int resp = urlConnection.getResponseCode();

            switch (resp){
                case HttpURLConnection.HTTP_OK:
                    try(InputStream is = urlConnection.getInputStream()){
                        bmp = BitmapFactory.decodeStream(is);
                    } catch(IOException e){
                        e.printStackTrace();
                    }
                    break;
                case HttpURLConnection.HTTP_UNAUTHORIZED:
                    break;
                default:
                    break;
            }
        } catch (Exception e) {
            Log.d("debug", "downloadImage error");
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
        }
        return bmp;
    }

    void setListener(Listener listener) {
        this.listener = listener;
    }

    interface Listener {
        void onSuccess(Bitmap bmp);
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スマホもPCの一員としてファイル同期したいじゃない?

前回の続きです。

前回はGo言語のお力を借りてWindows、Linuxが載ってるデバイス間は同期できたねー
でも僕らはたいてい他のコンピューターも抱えているハズ。

そう、スマホ!

こいつも前回のエコシステムに乗っけられればもっと家庭内ネットワークはいいものになるハズ。
PC達とスマホを同期する機会なんて無いって?そうでしょうか??僕は

  • NASに溜め込んだMP3をランダムで引っこ抜く
  • PC側でゲインとか音質とかバッチ処理で変換処理して標準化
  • スマホに転送する

って事をよくやるので必要で、こんな使い方が今回のツールを作る出発点だったりします

でけた!

andoukie.gif

このアプリはこんな機能があります。

  • アクセス先IPアドレスをQRコードで読みます。なので手入力の手間が要りません
  • ゲージとかスイッチで直感的に同期をコントロールできます
  • PC版みたく、クライアントしかないファイルを消して完全同期する機能があります

詳しくはリポジトリ見てくださいな。releaseの.apkをもってくればインスコできます。

つかいたい!

  • PC版を起動したら、EnterかSpaceを押すとQRコードが表示されます

1.png

  • それをスマホ版アプリで読み取れば同期が開始されます

UIわからん!

2.png

①は同期した時にサーバーに無いのに、クライアントにあるファイルを消すモードです。完全同期しておきたい時に使います。

②は同期ゲージです。この数字が何秒毎に同期するかになります。同期間隔が狭いとデカいファイルが転送しきれないのでその点はご注意を

③は同期状況とか動作ステータスが色々でます

あとがき

アプリの開発にApache cordovaを使いましたが、

・プラグインが継続保守されてなくて大量に開発停止してて動かないものが多い
・エラーを教えてくれない、のは良いがあってもスルーして動く(無い関数があっても平気で動く)
・デバッグしてデプロイしてもアプリを再インスコしないと修正できない

という僕的にうーんな点があって苦労した・・
これ流石に修正してコミットするレベル越えすぎなので逃げてすいませんが。。

追伸

Syncthing知らないで作ってしまったので参考に持ってよくできないか考え中っす!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ConstraintLayout入門その1 - RelativeLayoutとの違い

2020/07/26(Sun): ConstraintLayout入門その2の作成に伴い、リンクの追加、および若干の説明の追記を行いました。

ConstraintLayoutを使う理由

2016年からAndroidサポートライブラリによって提供されているConstraintLayoutは、非常に細かい制約の記述を用いた複雑なレイアウトを可能にします。FrameLayoutやLinearLayoutとの違いは、子Viewに対して親Layoutとの関係だけでなく、複数の子View同士の関係を記述することで細かく位置取りを制御できる点です。

しかし、そのようなレイアウトなら昔からRelativeLayoutで提供されています。にもかかわらずConstraintLayoutが新たに開発された理由はいくつかありますが、まずはConstraintLayout入門の手始めに、RelativeLayoutではサポートされていなかったレイアウトXMLの書き方を端的に示す例を紹介します。本稿を読んだ後、ConstraintLayoutは複雑すぎて敬遠していたけど使う気になった、という開発者が1人でも増えれば幸いです。

表示結果

今回はRelativeLayoutとConstraintLayoutの違いを示すために、両者を上下に並べて表示するアプリを作ります。以下のように表示されます。
表示結果
ともにボタン4つを横幅いっぱいに並べていますが、上はボタン4だけが横に伸びているのに対して、下はボタン1〜4が均等に並んでいます。上のように表示したいこともありますが、下のように均等に配列するレイアウトは使える場面が多そうです。本稿では、ConstraintLayoutをどのように使ってこの配列を実現しているかを解説します。

ConstraintLayoutを使うための設定

開発にはAndroid Studio 3.0 以降を使用します。
現在はAndroidXライブラリでConstraintLayoutが用意されていますので、これを使いましょう。まず build.gradle に以下の記述を追加します。

build.gradle
...
repositories {
    google()
    ...
}
...

app/build.gradle には以下の記述を追加しましょう。MotionLayoutなどをサポートしたconstraintlayoutバージョン2.0は本稿執筆時点でまだベータ版ですので、ここでは1.1安定版を使います。

app/build.gradle
...
dependencies {
    ...
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    ...
}

レイアウトXML

以下のようにRelativeLayoutとConstraintLayoutを並べています(便宜上、LinearLayoutを使用)。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <include layout="@layout/layout_relative" />

    <include layout="@layout/layout_constraint" />

</LinearLayout>

RelativeLayoutの記述は以下の通りです。

layout_relative.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    android:background="#B2CBE4"
    tools:showIn="@layout/activity_main">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:layout_alignParentTop="true"
        android:layout_alignParentStart="true"
        android:textColor="@android:color/black"
        android:textSize="20sp"
        android:text="RelativeLayout" />

    <Button
        android:id="@+id/btnR1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_alignParentStart="true"
        android:text="1" />

    <Button
        android:id="@+id/btnR2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_toEndOf="@id/btnR1"
        android:text="2" />

    <Button
        android:id="@+id/btnR3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_toEndOf="@id/btnR2"
        android:text="3" />

    <Button
        android:id="@+id/btnR4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_toEndOf="@id/btnR3"
        android:layout_alignParentEnd="true"
        android:text="4" />

</RelativeLayout>

ここで、ボタン1〜3は android:layout_alignParentStart および android:layout_toEndOf によって左端の位置を決めています。一方ボタン4は android:layout_toEndOfandroid:layout_alignParentEnd で左右両端の位置を決めています。

ConstraintLayoutの記述は以下の通りです。

layout_constraint.xml
<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    android:background="#CAEB5E"
    tools:showIn="@layout/activity_main">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_alignParentStart="true"
        android:textColor="@android:color/black"
        android:textSize="20sp"
        android:text="ConstraintLayout" />

    <Button
        android:id="@+id/btnC1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/btnC2"
        android:text="1" />

    <Button
        android:id="@+id/btnC2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@id/btnC1"
        app:layout_constraintEnd_toStartOf="@id/btnC3"
        android:text="2" />

    <Button
        android:id="@+id/btnC3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@id/btnC2"
        app:layout_constraintEnd_toStartOf="@id/btnC4"
        android:text="3" />

    <Button
        android:id="@+id/btnC4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@id/btnC3"
        app:layout_constraintEnd_toEndOf="parent"
        android:text="4" />

</androidx.constraintlayout.widget.ConstraintLayout>

ここでは、ボタン1〜4のそれぞれに app:layout_constraintStart_toStartOf, app:layout_constraintStart_toEndOf, app:layout_constraintStart_toEndOf, app:layout_constraintEnd_toEndOf のうち2つを使って、左右両端の位置を決めています。

また、コード (Kotlin) は以下の通りです。

MainActivity.kt
package com.example.constraintlayout1

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

循環依存??

RelativeLayoutでは3つのボタンで左端の位置しか設定していないため、左側のViewの位置が決まった後で右側のViewの位置が決まる、といった主と従の関係が発生することになり、不均等な配置になります。一方ConstraintLayoutでは4つのボタンすべてで左右両端の位置を設定しているので、均等な配置になります。したがって表示結果が異なるのは当然といえば当然なのでした。では、以下のようにXMLファイルを変更して、RelativeLayoutの方でもすべてのボタンの左右の位置を決めることにしてはどうでしょうか?

    ...
    <Button
        android:id="@+id/btnR1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_alignParentStart="true"
        android:layout_toStartOf="@id/btnR2"
        android:text="1" />

    <Button
        android:id="@+id/btnR2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_toEndOf="@id/btnR1"
        android:layout_toStartOf="@id/btnR3"
        android:text="2" />

    <Button
        android:id="@+id/btnR3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_toEndOf="@id/btnR2"
        android:layout_toStartOf="@id/btnR4"
        android:text="3" />
    ...

変更後、実行すると、

LogCat
com.example.constraintlayout1 E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.constraintlayout1, PID: 4174
    java.lang.IllegalStateException: Circular dependencies cannot exist in RelativeLayout
...

ビルドはできたものの、アプリは起動後すぐクラッシュしてしまいます。上記のLogCatを見ると「RelativeLayoutの中に循環的な依存性があってはいけない」といったエラーメッセージが書かれています。
そう言われてみると、ボタン1の右端はボタン2の左端の位置にある、ボタン2の左端はボタン1の右端にある、…等々と書いているので、ボタン2の位置が決まらないとボタン1の位置が決まらない、ボタン1の位置が決まらないとボタン2の位置が決まらない、…という堂々巡りになっていつまでも位置が決められない、エラーだ、という言い分はごもっとものように思えます。

ConstraintLayoutなら、循環制約を書いてもOK

ConstraintLayoutがRelativeLayoutと大きく異なる点は、このように循環的な制約の記述があってもエラーとせず解決できることにあります。そのことによって、ここではボタン1〜4がすべて平等に左右両端の制約を持つことにより、ボタン同士が相互に制約し合うことでレイアウト全体の位置を決定させることができます。
このように双方向の制約関係のあるViewの一団を、ConstraintLayoutではChain(チェーン、鎖)と呼び、いくつかパラメータを設定することによってさらに細かい調整ができるようになっています。が、ひとまずここでは「Viewがお互い平等につっぱり合うことで均等な配列が可能になる」という知識だけを持ち帰っていただければ充分です。より詳しい解説は、次の機会に譲ります。多機能かつ複雑なConstraintLayoutは、一気に全貌をマスターしなくても、簡単な機能から少しずつ習得していくことをお勧めします。

今回はViewの水平方向の制約にのみ注目しましたが、 layout_constraint.xml における各ボタンの垂直方向の制約は

<Button
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    />

の記述によってConstraintLayoutの中段に配置しています。このセンタリングの要領については、次の記事をご覧ください。

サンプルコード

今回のサンプルコードは以下のリポジトリにあります。
https://github.com/csayamada/ConstraintLayout1

参考文献

Constraintlayout | Android デベロッパー | Android Developers
ConstraintLayout | Android Developers
RelativeLayout | Android Developers

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FlutterFragmentを設置したらToolbarがStatus barにめり込んだ。対処法を解説。

Androidネイティブなアプリの一部機能をFlutterで開発するために、FlutterFragmentを設置しました。そうしたらAndroidネイティブなToolbarがStatus Barにめり込んでしまいました。

解決方法

レイアウトファイルに android:fitsSystemWindows="true" 属性を設定すればめり込まなくなります。

activity_pager.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context="jp.bellware.echo.view.pager.PagerActivity">

    <androidx.appcompat.widget.Toolbar
        app:layout_constraintTop_toTopOf="parent"
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/primary"
        android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
        android:elevation="5dp"
        android:minHeight="?attr/actionBarSize" />
    <!-- Toolbarの下全体にFlutterFragmentを設置する -->
    <FrameLayout
        android:id="@+id/fragmentContainer"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintTop_toBottomOf="@id/toolbar"
        app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

詳細

FlutterFragmentを設置すると window.decorView.systemUiVisibility が書き換わってしまいます。

FlutterFragmentを設置しなければ0ですが、設置すると

が設定されてしまいます。
確認にはActivityのonPauseメソッドでLogcatを使いました。

公式のFlutterFragmentの解説ではshouldAttachEngineToActivityという設定が紹介されていましたが、この設定を変更してもこの現象は変わりませんでした。

さて、 SYSTEM_UI_FLAG_LAYOUT_FULLSCREENSYSTEM_UI_FLAG_LAYOUT_STABLE がなにを表しているかというと、こちらの公式の解説でStatus barの後ろにアプリのコンテンツを設置できるとありました。さらに android:fitsSystemWindows 属性をtrueにすることでシステムウィンドウを避けたpaddingを設定出来るとあったので、今回もそれで解決できました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ViewPager2のFragmentのtag名はf0, f1, f2

ActivityからFragmentのメソッドを呼びたいときは、FragmentManagerクラスのfindFragmentByIdメソッドまたはfindFragmentByTagメソッドを使ってFragmentの参照を取得してメソッドを呼び出すことが公式で案内されています。

アクティビティと通信する

FragmentがViewPagerのページを構成しているときは、公式の記載は無いですが、このようなやり方でFragmentの参照を取得することができます。

ViewPagerのadapterFragmentPagerAdapterを使っている場合のみ有効です。FragmentStatePagerAdapterを使っている場合はFragmentの参照を取得できませんでした。

PagerActivity.kt
// 2ページ目FragmentのsomeMethodメソッドを呼び出す
val id = viewPager.id
val index = 1
val tag = "android:switcher:$id:$index"
val f = supportFragmentManager.findFragmentByTag(tag) as SomeFragment?
f?.someMethod()

今回、ViewPager2を使っている場合はどうすれば良いか調べてみました。
f0, f1, f2のように、よりシンプルなタグ名が付けられていました。

PagerActivity.kt
// 2ページ目FragmentのsomeMethodメソッドを呼び出す
val tag = "f1"
val f = supportFragmentManager.findFragmentByTag(tag) as SomeFragment?
f?.someMethod()

情報源(Stackoverflow)

公式情報では無いのでライブラリをアップデートしたときはご注意ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む