ViewConfigurationの動作の解説と検証【JetpackCompose】
JetpackComposeには、UIのタップやスクロール動作のタイムアウト、サイズに関するパラメーターを指定するためのViewConfigurationというインターフェースが存在します。
CompositionLocalProviderで、このインターフェースを実装したクラスを渡すことにより、ロングタップまでの時間などを変更することが可能になります。
この記事ではViewConfigurationのそれぞれのパラメータが、実際のUI動作にどのような影響を及ぼすかを解説し検証します。
- ViewConfigurationの作り方と渡し方
- longPressTimeoutMillis
- doubleTapTimeoutMillis
- doubleTapMinTimeMillis
- touchSlop
- minimumTouchTargetSize
- maximumFlingVelocity
ViewConfigurationの作り方と渡し方
JetpackComposeには、値のバケツリレーを抑制するためにCompositionLocalという仕組みが存在します。
CompositionLocalを用いてViewConfigurationを渡すことにり、渡されたブロックより内側の動作が変更されます。

自分でViewConfigurationを実装したクラスを作成するには、一からViewConfigurationを実装したクラスを定義するか、今渡されているViewConfigurationを継承するかの2つの方法があります。
基本的には今渡されているViewConfigurationを継承するのが一般的で楽なので、そちらの方法を採用します。
例えば、長押しのタイムアウト時間を1000msに設定したViewConfigurationを作成して渡す方法は次のコードのようになります。
@Composable fun ViewConfigurationProvideSample() { val defaultViewConfiguration = LocalViewConfiguration.current class SampleViewConfiguration : ViewConfiguration by defaultViewConfiguration { override val longPressTimeoutMillis: Long get() = 1000L } val sampleViewConfiguration = remember { SampleViewConfiguration() } CompositionLocalProvider( LocalViewConfiguration provides sampleViewConfiguration ) { // このブロック内では、長押しのタイムアウト時間が1000msに設定される } }
longPressTimeoutMillis
longPressTimeoutMillisは、長押し(ロングタップ、ロングプレス)として判定されるまでの時間をミリ秒で指定します。
サンプルアプリ
サンプルアプリでは、longPressTimeoutMillisを3000に設定しています。
3000msタップし続けると、長押し判定となっていることがわかります。
クリックしてソースコードを表示
@OptIn(ExperimentalFoundationApi::class) @Composable fun LongPressTimeoutMillsSurvey() { // region: タイマー val coroutineScope = rememberCoroutineScope() var timerStartTime by remember { mutableLongStateOf(System.nanoTime()) } var timerCurrentTime by remember { mutableLongStateOf(0L) } LaunchedEffect(Unit) { coroutineScope.launch(Dispatchers.Default) { while (true) { timerCurrentTime = (System.nanoTime() - timerStartTime) / 1000000L delay(10) } } } // endregion // region: 長押しまでの時間を3秒に設定するViewConfiguration val defaultViewConfiguration = LocalViewConfiguration.current class AppViewConfiguration : ViewConfiguration by defaultViewConfiguration { override val longPressTimeoutMillis: Long get() = 3000L // 長押しまでの時間を3秒に設定 } val appViewConfiguration = remember { AppViewConfiguration() } // endregion // region: 長押しが検出されたときのダイアログ var showLongPressDetectedDialog by remember { mutableStateOf(false) } if (showLongPressDetectedDialog) { AlertDialog( onDismissRequest = { showLongPressDetectedDialog = false }, confirmButton = { Button(onClick = { showLongPressDetectedDialog = false }) { Text(text = "OK") } }, title = { Text(text = "Long press detected") }, ) } // endregion // region: UI本体 CompositionLocalProvider( LocalViewConfiguration provides appViewConfiguration // ViewConfigurationを設定 ) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { Text(text = "Long press timer: $timerCurrentTime") Text( text = "Long press timeout: ${LocalViewConfiguration.current.longPressTimeoutMillis}ms", modifier = Modifier .background(Color(0xFFFF6666)) .combinedClickable( onClick = { }, onLongClick = { showLongPressDetectedDialog = true }, ) .pointerInput(Unit) { awaitEachGesture { awaitFirstDown(requireUnconsumed = true) timerStartTime = System.nanoTime() } } .padding(vertical = 16.dp) ) } } // endregion }
doubleTapTimeoutMillis
doubleTapTimeoutMillisは、2回目のタップがダブルタップとして判定されるまでのタイムアウト時間を指定します。

サンプルアプリ
サンプルアプリでは、doubleTapTimeoutMillisを2000に設定しています。
1回目のタップから、2000ms以内のタップがあればダブルタップ、2000ms以降では普通のタップとして認識されていることがわかります。
クリックしてソースコードを表示
@OptIn(ExperimentalFoundationApi::class) @Composable fun DoubleTapTimeoutMillsSurvey() { // region: タイマー val coroutineScope = rememberCoroutineScope() var timerStartTime by remember { mutableLongStateOf(System.nanoTime()) } var timerCurrentTime by remember { mutableLongStateOf(0L) } LaunchedEffect(Unit) { coroutineScope.launch(Dispatchers.Default) { while (true) { timerCurrentTime = (System.nanoTime() - timerStartTime) / 1000000L delay(10) } } } // endregion // region: ダブルタップのタイムアウト時間を2秒に設定するViewConfiguration val defaultViewConfiguration = LocalViewConfiguration.current class AppViewConfiguration : ViewConfiguration by defaultViewConfiguration { override val doubleTapTimeoutMillis: Long get() = 2000L // ダブルタップのタイムアウト時間を2秒に設定 } val appViewConfiguration = remember { AppViewConfiguration() } // endregion // region: ダブルタップが検出されたときのダイアログ var showDoubleTapDetectedDialog by remember { mutableStateOf(false) } if (showDoubleTapDetectedDialog) { AlertDialog( onDismissRequest = { showDoubleTapDetectedDialog = false }, confirmButton = { Button(onClick = { showDoubleTapDetectedDialog = false }) { Text(text = "OK") } }, title = { Text(text = "Long press detected") }, ) } // endregion // region: UI本体 CompositionLocalProvider( LocalViewConfiguration provides appViewConfiguration // ViewConfigurationを設定 ) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { Text(text = "Timer: $timerCurrentTime") Text( text = "Double tap timeout: ${LocalViewConfiguration.current.doubleTapTimeoutMillis}ms", modifier = Modifier .background(Color(0xFFFF6666)) .combinedClickable( onClick = { }, onDoubleClick = { showDoubleTapDetectedDialog = true } ) .pointerInput(Unit) { awaitEachGesture { awaitFirstDown(requireUnconsumed = true) timerStartTime = System.nanoTime() } } .padding(vertical = 16.dp) ) } } // endregion }
doubleTapMinTimeMillis
doubleTapMinTimeMillisは、ダブルタップとして扱われるまでの最小時間を指定します。
サンプルアプリ
サンプルアプリではdoubleTapMinTimeMillisを1000に設定しています。また、doubleTapTimeoutMillisは2000に設定しています。
1回目のタップから1000ms以下のタップはダブルタップとして認識されていません。
しかし、2回目以降(!)のタップが1000ms以降であればダブルタップとして認識されていることがわかります。
ここで注意するべきポイントは、2回目だけでなく2回目以降のタップでもダブルタップとして認識される点です。
クリックしてソースコードを表示
@OptIn(ExperimentalFoundationApi::class) @Composable private fun DoubleTapMinTimeMillsSurvey() { // region: タイマー val coroutineScope = rememberCoroutineScope() var timerStartTime by remember { mutableLongStateOf(System.nanoTime()) } var timerCurrentTime by remember { mutableLongStateOf(0L) } var clickHistoryTime = remember { mutableStateListOf<Long>() } LaunchedEffect(Unit) { coroutineScope.launch(Dispatchers.Default) { while (true) { timerCurrentTime = (System.nanoTime() - timerStartTime) / 1000000L delay(10) } } } // endregion // region: ダブルタップの最小時間を2秒に設定するViewConfiguration val defaultViewConfiguration = LocalViewConfiguration.current class AppViewConfiguration : ViewConfiguration by defaultViewConfiguration { override val doubleTapMinTimeMillis: Long get() = 1000L // ダブルタップの最小時間を1秒に設定 override val doubleTapTimeoutMillis: Long get() = 2000L // ダブルタップのタイムアウト時間を2秒に設定 } val appViewConfiguration = remember { AppViewConfiguration() } // endregion // region: ダブルタップが検出されたときのダイアログ var showDoubleTapDetectedDialog by remember { mutableStateOf(false) } if (showDoubleTapDetectedDialog) { AlertDialog( onDismissRequest = { showDoubleTapDetectedDialog = false }, confirmButton = { Button(onClick = { showDoubleTapDetectedDialog = false }) { Text(text = "OK") } }, title = { Text(text = "Double tap detected") }, ) } // endregion // region: UI本体 CompositionLocalProvider( LocalViewConfiguration provides appViewConfiguration // ViewConfigurationを設定 ) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { clickHistoryTime.forEachIndexed { index, historyTime -> val diff = if (index == 0) "-" else (historyTime - clickHistoryTime[index - 1]).toString() Text(text = "Click $index: $historyTime ($diff)") } Text(text = "Timer: $timerCurrentTime") Text( text = """ Double tap timeout: ${LocalViewConfiguration.current.doubleTapTimeoutMillis}ms Double tap min time: ${LocalViewConfiguration.current.doubleTapMinTimeMillis}ms """.trimIndent(), modifier = Modifier .background(Color(0xFFFF6666)) .combinedClickable( onClick = { }, onDoubleClick = { showDoubleTapDetectedDialog = true } ) .pointerInput(Unit) { awaitEachGesture { awaitFirstDown(requireUnconsumed = true) clickHistoryTime.add(timerCurrentTime) } } .padding(vertical = 16.dp) ) } } // endregion }
touchSlop
touchSlopは、スクロールまたはドラッグとして扱われるまでのスワイプの距離をピクセル単位で指定します。
サンプルアプリ
サンプルアプリではtouchSlopを128dp分のピクセル数として指定しています。
touchSlop未満のスクロールではスクロールとして扱われず、touchSlop以上ではスクロールとして扱われます。
また、Modifier.pointerInputのdetectDragGesturesが発生するスワイプのしきい値にもこの値が使用されます。
クリックしてソースコードを表示
@Composable private fun TouchSlopSurvey() { // region: TouchSlopを128に設定するViewConfiguration val defaultViewConfiguration = LocalViewConfiguration.current val density = LocalDensity.current val touchSlop = 128.dp val touchSlopPx = with(density) { touchSlop.toPx() } class AppViewConfiguration : ViewConfiguration by defaultViewConfiguration { override val touchSlop: Float get() = touchSlopPx // TouchSlopを128に設定 } val appViewConfiguration = remember { AppViewConfiguration() } // endregion val items = remember { (0..100).toList() } CompositionLocalProvider( LocalViewConfiguration provides appViewConfiguration // ViewConfigurationを設定 ) { Row(modifier = Modifier.fillMaxSize()) { Box( modifier = Modifier .background(Color.Red) .height(touchSlop) .width(8.dp) ) LazyColumn { items(items) { item -> Text( text = "Item $item", modifier = Modifier .background(if (item % 2 == 0) Color.Gray else Color.LightGray) .padding(16.dp) .fillMaxWidth() .weight(1f, fill = false), ) } } } } }
minimumTouchTargetSize
minimumTouchTargetSizeは、このサイズより小さい要素があったとき、タップ範囲が自動的にこのサイズまで拡大されます。
サンプルアプリ
サンプルアプリではminimumTouchTargetSizeを128dpに指定しています。
赤枠が128dp、緑枠が48dpとなっていて、赤枠をタップすればタップ判定が発生していることがわかります。
クリックしてソースコードを表示
@Composable private fun MinimumTouchTargetSizeSurvey() { val context = LocalContext.current val defaultViewConfiguration = LocalViewConfiguration.current // region: MinimumTouchTargetSizeを128dpに設定するViewConfiguration class MinimumTouchTargetSize128ViewConfiguration : ViewConfiguration by defaultViewConfiguration { override val minimumTouchTargetSize: DpSize get() = DpSize(128.dp, 128.dp) // MinimumTouchTargetSizeを128dpに設定 } val minimumTouchTargetSize128ViewConfiguration = remember { MinimumTouchTargetSize128ViewConfiguration() } // endregion CompositionLocalProvider( LocalViewConfiguration provides minimumTouchTargetSize128ViewConfiguration // ViewConfigurationを設定 ) { Row( modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier .size(128.dp) .background(Color.Red), contentAlignment = Alignment.Center, ) { Box( modifier = Modifier .size(48.dp) .background(Color.Green) .clickable { Toast .makeText(context, "Clicked", Toast.LENGTH_SHORT) .show() } ) } } } }
maximumFlingVelocity
maximumFlingVelocityは、スクロールのフライング動作(スクロールに速度がついて勝手に一定距離スクロールする動作)の最大速度をピクセル単位で指定します。
サンプルアプリ
サンプルアプリではmaximumFlingVelocityを600dp分のピクセル数として指定しています。
スクロールのフライング動作が落ちていることがわかります。
クリックしてソースコードを表示
@Composable private fun MaximumFlingVelocitySurvey() { // region: MaximumFlingVelocityを600dp/sに設定するViewConfiguration val defaultViewConfiguration = LocalViewConfiguration.current val density = LocalDensity.current val maximumFlingVelocity = 600.dp val maximumFlingVelocityPx = with(density) { maximumFlingVelocity.toPx() } class AppViewConfiguration : ViewConfiguration by defaultViewConfiguration { override val maximumFlingVelocity: Float get() = maximumFlingVelocityPx // MaximumFlingVelocityを600dp/sに設定 } val appViewConfiguration = remember { AppViewConfiguration() } // endregion val items = remember { (0..100).toList() } CompositionLocalProvider( LocalViewConfiguration provides appViewConfiguration // ViewConfigurationを設定 ) { Row(modifier = Modifier.fillMaxSize()) { Box( modifier = Modifier .background(Color.Red) .height(maximumFlingVelocity) .width(8.dp) ) LazyColumn { items(items) { item -> Text( text = "Item $item", modifier = Modifier .background(if (item % 2 == 0) Color.Gray else Color.LightGray) .padding(16.dp) .fillMaxWidth() .weight(1f, fill = false), ) } } } } }
DjangoのValidationErrorのエラーメッセージ一覧を出力したい時、[error.message for error in ValidationError.error_list]を使うとハマるかもしれない話
こんにちは。うぃんちゃんです。
DjangoでValidationErrorのエラーメッセージを出力したい時に「ValidationError.error_list」を使うと、エラーメッセージがフォーマットされずハマる可能性があります。
結論
error変数をValidationError型とすると、
[inner_error_message for inner_error_message in error]
でOK。
どのような時にハマるか
[error.message for error in ValidationError.error_list]
のように書いた時、エラーメッセージがフォーマットされずに出力されてしまいます。
具体例を挙げると、Djangoのパスワードバリデーションが正しいかどうか次のviewを書いたとします。
@api_view(['POST']) def validate_password(request): if 'password' not in request.data: return Response(status=400) try: password_validation.validate_password(request.data['password']) return JsonResponse({ 'result': True }) except ValidationError as error: return JsonResponse({ 'result': False, 'error_messages': [error.message for error in error.error_list] })
すると、最低文字数のエラーメッセージがフォーマットされずに次のように表示されてしまいます。
このパスワードは短すぎます。最低 %(min_length)d 文字以上必要です。
原因
本来、ValidationErrorをループで回すと__iter__関数により次の処理が走って正しい文字列にフォーマットされてくれます。
def __iter__(self): if hasattr(self, 'error_dict'): for field, errors in self.error_dict.items(): yield field, list(ValidationError(errors)) else: for error in self.error_list: message = error.message if error.params: message %= error.params # ここでフォーマットされてくれる yield str(message)
しかし、error_listからerrorを直接持ってきてmessageを取り出してしまうとフォーマットされません。
対策
冒頭に書いたとおり、
[inner_error for inner_error in error]
でOKです。
errorをループで回すと勝手に文字列にフォーマットされてエラー一覧の文字列の配列になります。
Vue.jsで条件によって動的にコンポーネントを変更[Vue.js+TypeScript]
お久しぶりです。
皆さんはVue.jsを使用していて、条件によってVue.jsのコンポーネントを変更したいということはありませんか?
if文で分けていく方法もありますが、コンポーネントや条件が多くなっていくとどんどん読みにくくなっていってしまいます。
そういった際に、どのような方法でプログラムを組めばいいかという一例を紹介します。
実装はVue.jsとTypeScriptを使ってやっていきます。
例として使用するプログラムの構成とイメージ

状況
データ(投票数とか、売上とか)の集計結果を表示したいが、データによってグラフを変えたり表示方法を変更したいなどの理由で、データの種類毎にコンポーネントを変更したい。
ソースコード
Factories.ts
import { VueConstructor } from "vue"; export abstract class Factory { /** * このFactoryクラスが、「引数に指定されたデータを 表示するためのコンポーネント」に変換するためのFactoryである場合はtrueを返します。 * @param data データ */ abstract isFactoryFor(data: Data): boolean; /** * 渡されたデータを表示するためのコンポーネントを生成します。 * @param data */ abstract generateComponent(data: Data): VueConstructor<Vue>; static addFactory(factory: Factory): void { this.factories.push(factory); } static findFactory(data: Data): Factory|undefined { return this.factories.find(f => f.isFactoryFor(data)); } static factories: Factory[] = []; } class PieChartFactory extends Factory { isFactoryFor(data: Data): boolean { //データの種類がPieChartを表示したい時にtrue } generateComponent(data: Data): VueConstructor<Vue> { //コンポーネントを生成 //例: //return PieChartComponent; } } class LineChartFactory extends Factory { isFactoryFor(data: Data): boolean { //データの種類がLineChartを表示したい時にtrue } generateComponent(data: Data): VueConstructor<Vue> { //コンポーネントを生成 //例: //return LineChartComponent; } } class TextAnswerFactory extends Factory { isFactoryFor(data: Data): boolean { //データの種類がTextAnswerを表示したい時にtrue } generateComponent(): VueConstructor<Vue> { //コンポーネントを生成 //例: //return TextAnswerComponent; } } Factory.addFactory(new PieChartFactory()); Factory.addFactory(new LineChartFactory()); Factory.addFactory(new TextAnswerFactory());
Graphs.vue
<template> <div> <component v-for="data in datas" :is="toChartComponent(data)" :data="data" /> </div> </template> <script lang="ts"> import Vue from "vue"; import Component from "vue-class-component"; import { Factory } from "Factories"; @Component export default class App extends Vue { datas: Data[]|null = []; //データの配列。何らかの方法でデータを入れる toChartComponent(data): VueConstructor<Vue> { return Factory.findFactory(data).generateComponent(); } } </script> <style lang="scss" scoped> </style>
※実際に使用する時は、各自で必要なところを変更してください。
プログラムの解説
原理としては、Factoryパターンを使用して実装しています。
まず、データの種類毎に、Factoryクラスを作っていってそれを登録(Factory.addFactory)していきます。
次に、toChartComponent(Graphs.vue内)でFactory.findFactoryを呼んであげて、データの種類によって使用するFactoryを分けています。
そして、generateComponentを呼んであげてコンポーネントを生成しています。
そして、vueのtemplate内にcomponentタグを作成してあげて「:is」でtoChartComponentを呼ぶことによって生成するコンポーネントを動的に変更することができます。
デモ
See the Pen
Blog-How-To-Change-Vuejs-Component-Dynamically by wintermaples (@wintermaples)
on CodePen.
※グラフの表示は省略しています。
平成30年 春期 基本情報・応用情報 偏差値表(大体)
自分が基本情報に受かったときも作ったけど、今回応用情報を受けて合格しましたのでまた作ってみました。
正規分布に従うと過程すればもっと正確に近い偏差値が出せるんですが、めんどくさいのでだいぶはしょって計算しています。
応用情報 午前
| 点数 | 人数 | 偏差値 |
|---|---|---|
| 90~100 | 107 | 76.91509161 |
| 80~89 | 1474 | 69.37786546 |
| 70~79 | 5052 | 61.8406393 |
| 60~69 | 8194 | 54.30341314 |
| 50~59 | 8048 | 46.76618698 |
| 40~49 | 5330 | 39.22896083 |
| 30~39 | 1951 | 31.69173467 |
| 20~29 | 260 | 24.15450851 |
| 10~19 | 17 | 16.61728235 |
| 0~9 | 2 | 9.080056197 |
μ=59.29045507
σ=13.26748036
応用情報 午後
| 点数 | 人数 | 偏差値 |
|---|---|---|
| 90~100 | 21 | 79.92948455 |
| 80~89 | 386 | 71.68944914 |
| 70~79 | 2099 | 63.44941373 |
| 60~69 | 4411 | 55.20937832 |
| 50~59 | 4489 | 46.96934291 |
| 40~49 | 2474 | 38.7293075 |
| 30~39 | 698 | 30.48927209 |
| 20~29 | 136 | 22.24923668 |
| 10~19 | 32 | 14.00920127 |
| 0~9 | 4 | 5.769165863 |
μ=58.6779661
σ=12.13587018
基本情報 午前
| 点数 | 人数 | 偏差値 |
|---|---|---|
| 90~100 | 177 | 73.99590791 |
| 80~89 | 2823 | 66.97312243 |
| 70~79 | 9502 | 59.95033696 |
| 60~69 | 11848 | 52.92755148 |
| 50~59 | 9490 | 45.904766 |
| 40~49 | 6182 | 38.88198052 |
| 30~39 | 2936 | 31.85919504 |
| 20~29 | 757 | 24.83640957 |
| 10~19 | 39 | 17.81362409 |
| 0~9 | 6 | 10.79083861 |
μ=60.83135283
σ=14.23936418
基本情報 午後
| 点数 | 人数 | 偏差値 |
|---|---|---|
| 90~100 | 625 | 73.60067585 |
| 80~89 | 2605 | 68.1938841 |
| 70~79 | 5285 | 62.78709234 |
| 60~69 | 8108 | 57.38030059 |
| 50~59 | 9928 | 51.97350883 |
| 40~49 | 9744 | 46.56671708 |
| 30~39 | 7908 | 41.15992533 |
| 20~29 | 4588 | 35.75313357 |
| 10~19 | 1500 | 30.34634182 |
| 0~9 | 437 | 24.93955006 |
μ=51.3499448
σ=18.49525644
ただし上記は次のように計算した。
ある得点範囲Aの下限をkとする。(例えば、30~39点ならk=30)
Aの範囲内の得点を取った人数をn(A)とするとき、k+5点を取った人がn(A)人であると仮定する。(例えば、基本情報午後の30~39点なら、n(30~39)=7908)
あとは正規分布に従うと仮定して、偏差値を通常通り導出する。
ちなみに自分の結果はこんな感じです。

午前偏差値: 66.55
午後偏差値: 67.57
基本情報よりなぜかかなり上がった(*´艸`*)
ItaEditor 1.1.2をリリースしました[ItaEditor][NetBeans][java][プログラミング]
修正遅れて申し訳ありません。m_m
バグ修正
背景画像タブがなくなることがあるのを修正
まだ完全にバグが解消されたかどうかを確認したわけではないのでまだバグが残ってる可能性があります。
何かあればコメントお願い致します。