コベリンの最近の取り組みとして、業務などで役立ちそうな知見を共有する会を開催することになりました。 そのついでに発表に使ったアジェンダもそのまま公開してしまおうという豪快な企画です。
※ アジェンダをそのままコピペして公開したものなので若干見にくい箇所もあるかもしれませんが、ご了承ください。
- MediaActionSoundとイヤホン出力 @takkumattsu
- Androidで複雑なグリッドレイアウトを実現する方法3選 @numa08 id:numanuma08
- Typescript の真の const @mironal
- 税について @yanac
MediaActionSoundとイヤホン出力 @takkumattsu
発生した問題
アナログイヤホンを挿している時にMediaActionSoundで音を鳴らすとイヤホンから鳴っている音が端末のスピーカーから鳴ってしまう
詳細
MediaActionSoundというOSのカメラ関連の音使う時に使うクラス。 自前でカメラアプリ的なのを作った時にシャッター音とかを鳴らすためのクラス。
別途MediaPlayerなどで音を再生している状態でMediaActionSoundで音を鳴らすと音の出力がイヤホン側から端末側に切り替わってしまう。
MediaActionSoundはシャッター音とかなのでマナーモードを貫通して音を鳴らしたりする挙動があると思うが、それに付随したバグな気がしている。
stackoverflowで検索しても全然情報はなかった
解決方法
MediaPlayerを使って自前で鳴らす
camera_click.oggみたいな音声ファイルが/system/media/audio/ui/
にあるのでそれを鳴らしている感じ。
public enum SystemSoundType { CameraClick("camera_click.ogg"), VideoRecord("VideoRecord.ogg"), VideoStop("VideoStop.ogg"); final String fileName; SystemSoundType(String fileName){ this.fileName = fileName; } }
import android.content.Context; import android.media.MediaPlayer; import android.net.Uri; import java.io.File; public class SystemSoundPlayer { private MediaPlayer mAudioPlayer; public void play(Context context, SystemSoundType type) { try { String filePath = filePath(type.fileName); mAudioPlayer = MediaPlayer.create(context, Uri.parse(filePath)); mAudioPlayer.setOnCompletionListener(mp -> { mAudioPlayer.reset(); mAudioPlayer.release(); mAudioPlayer = null; }); if (mAudioPlayer != null) { mAudioPlayer.start(); } } catch (Exception e) { if (mAudioPlayer != null) { mAudioPlayer.reset(); mAudioPlayer.release(); mAudioPlayer = null; } } } private String filePath(String fileName) throws Exception { // https://android.googlesource.com/platform/frameworks/base/+/master/media/java/android/media/MediaActionSound.java#51 より for (String path : new String[]{"/product/media/audio/ui/", "/system/media/audio/ui/",}) { File f = new File(path + fileName); if (f.exists()) { return path + fileName; } } throw new Exception(fileName + "のサウンド音が見つかりませんでした"); } }
ほとんど https://android.googlesource.com/platform/frameworks/base/+/master/media/java/android/media/MediaActionSound.java と同じ実装だけどね。 MediaActionSoundはSoundPoolクラスを使って鳴らしているっぽい。
FLAG_AUDIBILITY_ENFORCEDが原因っぽいかな?
mSoundPool = new SoundPool.Builder() .setMaxStreams(NUM_MEDIA_SOUND_STREAMS) .setAudioAttributes(new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build()) .build();
Androidで複雑なグリッドレイアウトを実現する方法3選 @numa08 id:numanuma08
グリッド上にレイアウトされた画像の上下に複雑なレイアウトがあり、さらに画面全体がスクロールする画面を実装する必要があったのでその実装方法としてどんなものがあるか考えた。
前提条件
- グリッド部分に表示される画像は動的に決まるが、個数は20個未満と少ない
- グリッド部分以外の要素も動的に決まるものがおおく、高さは要素の内容で変わってくる
今回はサンプルとして、ヘッダー部分に「吾輩は猫である」の冒頭部分のテキストを、画像として猫の画像を3列で20個表示するアプリを作ってみた。
flutterを使うバージョン
- 簡単さ :◎
- 導入しやすさ: △
- 既存プロジェクトがflutterなら問題はない。
CustomScrollView
のsliver
リストに要素をSliverList
やSliverGrid
を使って並べていけば実現可能。CustomScrollViewはネストしたスクロール可能なViewをどんどん並べていくことで、flutterらしくレイアウトを宣言的に記述できる。
Scaffold( appBar: AppBar(), // スクロールする部分をCustomScrollViewで囲む body: CustomScrollView( // sliversに内部の要素を入れていく slivers: [ // ヘッダー部分はSliverListを使う SliverList( delegate: SliverChildListDelegate([ Text(''' // 省略 ''') ])), // グリッド部分はSliverGridを使う SliverGrid( delegate: SliverChildBuilderDelegate( (context, count) { return Image.network('https://placekitten.com/400/400'); }, childCount: 20, ), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, childAspectRatio: 1, ), ) ], ), );
epoxyを使うバージョン
- 簡単さ: ○
- 導入しやすさ: △
- 依存ライブラリの定義とアノテーションプロセッサの設定が必要
- 独自のdsl的記法の習得も必要
airbnbのepoxyを使うと1つのRecyclerView内の要素をControllerを使って宣言的に書ける。Controllerのコードだけを見てみるとflutterのような手軽さがある。
fragmentのレイアウトファイル
EpoxyRecyclerView
を画面に表示する。LayoutManagerにGridLayoutManager
を指定し、spanCount=3
として画像を3列で並べられるようにする。
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" tools:context=".EpoxyFragment"> <com.airbnb.epoxy.EpoxyRecyclerView android:id="@+id/recycler" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:spanCount="3" tools:listitem="@layout/epoxy_view_holder_list_item_grid_image" /> </layout>
ヘッダーのレイアウトファイル
こちらも今回はTextView
を1つもつだけ。改行を含むテキストをリソースで定義するのがだるかったので、DataBindingでkotlinのコードから指定する。
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="text" type="String" /> </data> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{text}" tools:text=" " /> </layout>
画像のレイアウトファイル
こちらもImageView
があるだけ。BindingAdapter
を使ってURLを指定するとGlideで画像を表示するがそのあたりは本筋じゃないので省略。
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="imageUrl" type="String" /> </data> <ImageView android:id="@+id/image" android:layout_width="match_parent" android:layout_height="wrap_content" app:imgUrl="@{imageUrl}" tools:ignore="ContentDescription" tools:src="@mipmap/ic_launcher" /> </layout>
EpoxyController
TypedEpoxyController
のサブクラスを作り、buildModels
内でヘッダー部分と画像のグリッド要素を表示する。この部分だけに注目するとflutterのような宣言的な書き方ができている。
class EpoxyListController : TypedEpoxyController<List<String>>() { override fun buildModels(data: List<String>) { // ヘッダー部分に吾輩は猫であるの冒頭文を挿入する headerView { id("header") text( """ // 省略 """.trimIndent() ) // spanSizeOverride で3列分の幅を確保する spanSizeOverride { _, _, _ -> 3 } } data.forEachIndexed { idx, url -> // パラメータに渡されたURLリストを使って、画像の表示をする。 listItemGridImage { id(idx) imageUrl(url) } } } }
Fragment
onViewCreated
でコントローラーにデータをセットする。
class EpoxyFragment : Fragment(R.layout.fragment_epoxy) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val controller = EpoxyListController() val binding = FragmentEpoxyBinding.bind(view) binding.apply { recycler.setController(controller) } controller.setData((1..20).map { "https://placekitten.com/400/400" }) } }
ConstraintLayoutとFlowを使う
- 簡単さ: △
- 導入しやすさ: ◎
- 今どきConstraintLayoutを使ってないAndroidのプロジェクトなんてある・・・?(煽
Flowを使うと要素をグリッド状に並べられる。レイアウトで定義したConstraintLayoutとFlowにFragmentから動的に画像のViewを挿入して実現する。しかし、xmlで定義したレイアウトにコードから動的に要素を追加するのでわかりにくいっちゃわかりにくい。またRecyclerViewを使わないので、要素が増えてくると描画のパフォーマンス低下が懸念される。
Fragmentのレイアウト
ScrollView
の中にConstraintLayout
を配置し、Flow
を配置する。Flowはflow_maxElementsWrap="3"
、low_wrapMode="aligned"
を指定すると3列のグリッド表示を実現できる。
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" tools:context=".FlowFragment"> <data> <variable name="headerText" type="String" /> </data> <ScrollView android:id="@+id/scroll" android:layout_width="match_parent" android:layout_height="wrap_content"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/header_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{headerText}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <androidx.constraintlayout.helper.widget.Flow android:id="@+id/grid_flow" android:layout_width="match_parent" android:layout_height="wrap_content" app:flow_maxElementsWrap="3" app:flow_wrapMode="aligned" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/header_title" /> </androidx.constraintlayout.widget.ConstraintLayout> </ScrollView> </layout>
Fragment
Flowを使うと要素のレイアウトはしてくれるが、サイズの決定はしないので事前に計算する必要がある。このあたりが専用のGridを使うケースと違うところ。要素に表示するViewを生成したらConstraintLayoutとFlow両方にaddViewしなければならない。
class FlowFragment : Fragment(R.layout.fragment_flow) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val binding = FragmentFlowBinding.bind(view) binding.apply { headerText = """ 吾輩わがはいは猫である。名前はまだ無い。 //省略 """.trimIndent() // 画像の横幅を計算する。今回は画面の幅の1/3 val imageWidth = this@FlowFragment.requireActivity().windowManager.defaultDisplay.width / 3 repeat(20) { // ImageViewを生成したら、ConstraintLayoutとFlowの両方にaddViewする val imageView = ImageView(requireContext()).apply { id = View.generateViewId() Glide.with(this) .load("https://placekitten.com/400/400") .override(imageWidth) .into(this) } container.addView( imageView ) gridFlow.addView(imageView) } } } }
Jetpack Component
- 簡単さ: ◎
- 導入しやすさ: △
Jetpack Composeを使えばkotlinのコードで宣言的にレイアウトの実装ができる。グリッドを実現するLazyVerticalGrid
もあるが複雑なレイアウトを実現するときはLazyColumn
の方が良さそう。とは言え、2021/07/28時点でrc-02でAndroid StudioもCanary版が必要となるため稼働中のプロジェクトで使う場合は慎重になる。
Fragment
LazyColumn
を使って最初にヘッダー部分のレイアウトを表示する。グリッドは表示する要素のリストをkotlinのwindowed
を使ってカラムの数分のリストに分割してRow
で描画する。余った部分は空のBox
を描画する。このとき、Modifier.weight(1f)
を使うといい感じに横幅の計算が行われる。
@ExperimentalFoundationApi @Composable fun BodyLayout() { LazyColumn { item { Text( """ 吾輩わがはいは猫である。名前はまだ無い。 // 省略 """.trimIndent(), ) } items((1..20).map { R.drawable.place_kitten } // Coilで画像を表示しようとしたが、なぜかうまく行かなかったのでローカルファイルを読み込む // windowedを使って[[0, 1, 2], [3, 4, 5], [6, 7]]のようにカラムの数分のネストしたリストを作り出す .windowed(size = 3, step = 3, partialWindows = true)) { list -> // Rowで横に描画 Row { repeat(3) { index -> val imageId = list.getOrNull(index) // 1行に3列分の要素がないときは、空のBoxを描画する if (imageId != null) { Image( painter = painterResource(id = imageId), contentDescription = null, modifier = Modifier.weight(1f) ) } else { Box(modifier = Modifier.weight(1f)) } } } } } } class ComposeFragment : Fragment() { @ExperimentalFoundationApi override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return ComposeView(requireContext()).apply { setContent { BodyLayout() } } } }
所感
複雑なレイアウトの実装となると、flutterが一番楽そう。しかし既存プロジェクトがflutterでは無い場合つらみがあるので、そうなるとepoxyやJetpack Composeを使うのが良いようにも思える。その一方、epoxyの場合静的なレイアウト部分が多くなると恩恵もすくなくなるためConstraintLayoutでの実現も検討に上がる。Jetpack Composeは速いところ正式リリースしてほしい。
Typescript の真の const @mironal
const obj = { hoge: "aaaaa" } // エラーにならない (オノレ...) obj.hoge = "bbbb"
辞書っぽい定数定義するときに困るね. 以下のような config を作っても定数にならないからミスりそう.
const APP_CONFIG = { key: "app key", secret: "app secret" }
どうする?
as const
する. const assertion
と呼ぶ.
const obj = { hoge: "aaaaa" } as const // Error: Cannot assign to 'hoge' because it is a read-only property.(2540) obj.hoge = "bbbb"
こうすると obj の型は以下のようになる
const obj: { readonly hoge: "aaaaa" }
以下ではない.
const obj: { readonly hoge: string }
Object.freeze
は?
const obj = Object.freeze({ hoge: "aaaaa", aaa: {c:"ccc"} }) // 代入できちゃうんだなコレが obj.aaa.c = "bbbb" // as const ならエラーになる const obj = { hoge: "aaaaa", aaa: {c:"ccc"} } as const // Error: Cannot assign to 'c' because it is a read-only property.(2540) obj.aaa.c = "bbbb" // as const したほうはこういう型になる(手書きすると大変だね) const obj: { readonly hoge: "aaaaa"; readonly aaa: { readonly c: "ccc"; }; }
実用的か知らないが、こういうふうに部分的に const にもできる
const VALUE = "CONSTANT VALUE" as const const objIncludeConst = { val: VALUE, huga: "1234" } // Type '"aaaa"' is not assignable to type '"CONSTANT VALUE"'.(2322) objIncludeConst.val = "aaaa" // こっちは変更できる objIncludeConst.huga = "ccc"
参考情報
- PR: https://github.com/Microsoft/TypeScript/pull/29510
- Widening / NonWidening: https://qiita.com/Takepepe/items/2c06f65a51a12ffe4d61
税について @yanac
https://docs.google.com/presentation/d/1ROVoo7ansnhbzIwdk_zRm1bPBOKaUh-1xu_1xZ819Jc/edit?usp=sharing