visible true

技術的なメモを書く

bit全探索 in Kotlin

最近AtCoderなどに参加していて、すべての組み合わせを生成しつつ計算するといった機会になんどか遭遇し、毎回頑張って実装していたのだけど、bit全探索という方法があるらしいと知り、調べて、Kotlinでどう書くか考えた結果次のようになった。

import java.util.BitSet

fun bitFullSearch(n: Int): List<BitSet> = (0 until (1 shl n)).map { bit ->
    BitSet(n).apply {
        repeat(n) { i ->
            set(i, bit and (1 shl i) > 0)
        }
    }
}

たとえば bitFullSearch(4) などと呼び出すと、それぞれ次のbitが立ったBitSetのリストが手に入る。

{}
{0}
{1}
{0, 1}
{2}
{0, 2}
{1, 2}
{0, 1, 2}
{3}
{0, 3}
{1, 3}
{0, 1, 3}
{2, 3}
{0, 2, 3}
{1, 2, 3}
{0, 1, 2, 3}

bitSet.get(i) でそのインデックスのビットが有効かBooleanが手に入るほか、bitSet.stream()で有効なインデックスのストリームが手に入るので大体いい感じにできる。

Material-UIのHidden要素をテストする

個人のプロジェクトでMaterial-UIを使っているんですが、 コンポーネントHiddenを含んでいると、テストがうまく動きません。

hoge.tsx

import * as React from 'react';
import {Hidden} from "@material-ui/core";

export const Hoge = (): JSX.Element => {
  return (
    <div>
      <Hidden mdUp>
        <div>mdUp</div>
      </Hidden>
      <Hidden smDown>
        <div>smDown</div>
      </Hidden>
    </div>
  )
};

hoge.test.tsx

import React from "react";
import {render, cleanup} from '@testing-library/react'
import {Hoge} from "../hoge";

afterEach(async () => {
  await cleanup();
});

describe('hoge', () => {
  describe('ある幅以上の時のレンダリング', () => {
    it('mdUpが表示される', () => {
      const {getByText} = render(
          <Hoge/>
      );
      expect(getByText("mdUp")).toBeVisible()
    });
  });
});

見つけられない...ていうかそもそもHidden要素がどっちもレンダリングされていない

TestingLibraryElementError: Unable to find an element with the text: mdUp. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

    <body>
      <div>
        <div />
      </div>
    </body>

      13 |           <Hoge/>
      14 |       );
    > 15 |       expect(getByText("mdUp")).toBeVisible()
         |              ^
      16 |     });
      17 |   });
      18 | });

環境

"@material-ui/core": "^4.10.2",
"react": "^16.13.1",
    
"@testing-library/dom": "^7.16.3",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.3.0",
"@testing-library/user-event": "^12.0.7"

原因など

Unable to test React with Material-UI Hidden element · Issue #2179 · enzymejs/enzyme · GitHub によると、jest-domとmaterial-uiの双方に課題があるようです。 -> https://github.com/enzymejs/enzyme/issues/2179#issuecomment-528973289

対応

対応方法は2つほどあるぽいです、ここではかんたんな方を書きます。 テスト側でrenderするときにcreateMuiThemeを用いて初期の画面幅を指定するやりかたです。

import React from "react";
import {render, cleanup} from '@testing-library/react'
import {createMuiTheme, MuiThemeProvider} from "@material-ui/core";
import {Hoge} from "../hoge";

afterEach(async () => {
  await cleanup();
});

describe('hoge', () => {
  describe('ある幅以上の時のレンダリング', () => {
    it('mdUpが表示される', () => {
      const theme = createMuiTheme({props: {MuiWithWidth: {initialWidth: 'sm'}}})
      const {getByText} = render(
        <MuiThemeProvider theme={theme}>
          <Hoge/>
        </MuiThemeProvider>
      );
      expect(getByText("mdUp")).toBeVisible()
    });
  });
});

これで画面幅を指定して実行できます。 initialWidthをmdにするとsmDownの方の要素が表示状態になります。

おわりに

ReactでTDDでやっていくぞと思って触り始めたけどやはりUI部分はなかなか大変だなと思いつつ、Androidよりは楽かもという気持ち

Jetpack Compose 0.1.0-dev05から0.1.0-dev06にしたときに変更が必要だったところ

前回に引き続いて。

left, rightがstart, endに

レイアウトのleft, rightstart, endになりました。なるだろうなーと思ってたので想定どおり。

before

LayoutPadding(
  top = 16.dp
  left = 8.dp,
  right = 8.dp,
  bottom = 16.dp
)

after

LayoutPadding(
  top = 16.dp
  start = 8.dp,
  end = 8.dp,
  bottom = 16.dp
)

DrawImageを廃止

Imageを描画するDrawImageがなくなりました。代わりにSimpleImageを使います。このあたりは今後も色々と変わりそうですね。

before

Container(
  width = 100.dp,
  height = 200.dp
) {
  DrawImage(image)
}

after

Container(
  width = 100.dp,
  height = 200.dp
) {
  SimpleImage(image)
}

androidx.compose.Contextを廃止

ContextAmbientはandroidx.compose.Contextを返してましたが、androidx.compose.Contextになりました。

before

import androidx.compose.Context

after

import android.content.Context

AppBarIconを廃止

TopAppBarのnavigationIconに使うAppBarIconがなくなり、代わりにIconButtonを使う形になりました。

before

TopAppBar(
  title = {
    Text(context.getString(R.string.app_name))
  },
  navigationIcon = {
    AppBarIcon(
      icon = ImagePainter(BitmapImage(context.getBitmap(R.drawable.ic_baseline_arrow_back_24))),
      onClick = {
        backStack.pop()
      }
    )
  }
)

IconButtonを使う場合のほうが冗長ですが、children: @Composable() () -> Unitを受け取るのでより柔軟な表現が可能になってます(例えばTextを渡してもちゃんと動く)。

after

TopAppBar(
  title = {
    Text(context.getString(R.string.app_name))
  },
  navigationIcon = {
    IconButton(
      onClick = {
        backStack.pop()
      }
    ) {
      SimpleImage(
        BitmapImage(context.getBitmap(R.drawable.ic_baseline_arrow_back_24))
      )
    }
  }
)

ArrangementにVertical, Hotizontalの概念を追加

Column, RowともにArrangementが設定できますが、寄せる方向の設定がStart, Endという名前でした。なので次のように同じ値でもColumnかRowかで意味が異なります。

before

Column(
  modifier = LayoutWidth.Fill,
  arrangement = Arrangement.End // 下寄せ
) {
  Row(
    modifier = LayoutWidth.Fill,
    arrangement = Arrangement.End // 右寄せ
  ) {
    // something
  }
}

ArrangementにVertical, Hotizontalの概念を追加し、使える値を増やしつつ制限をかけています。これにより意味を理解しやすくなりました。

after

Column(
  modifier = LayoutWidth.Fill,
  arrangement = Arrangement.Bottom // Arrangement.Endは使えない
) {
  Row(
    modifier = LayoutWidth.Fill,
    arrangement = Arrangement.End
  ) {
    // something
  }
}

おわりに

今までは1ヶ月に1リリースという感じでしたが、2月は2回ありました。I/Oに向けてガガガッとスパートかけてる感じなんでしょうか。楽しみですね。

Jetpack Compose 0.1.0-dev05で追加されたAdapterListを眺める

Jetpack Compose 0.1.0-dev05がリリースされましたね。0.1.0-dev05でui-foundationAdapterListというComposableが追加されました。

待望のAdapterList

AdapterListの説明は次のようになっています。

A vertically scrolling list that only composes and lays out the currently visible items.

今まではRecyclerViewのようなComposableが存在せず、VerticalScrollerを使ってそれっぽい動作をしていましたが、VerticalScrollerはScrollViewと同じものなので実用には限界がありました。

AdapterListはRecyclerViewと同じように、表示されている要素だけをレンダリングするので、大量の要素があってもサクサク動作します。

AdapterListの使い方

AdapterListのシグネチャは次の通りです。

@Composable
fun <T> AdapterList(
  data: List<T>,
  modifier: Modifier = Modifier.None,
  itemCallback: @Composable() (T) -> Unit
)

実際に使うには次のようになります。

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.ui.core.Text
import androidx.ui.core.setContent
import androidx.ui.foundation.AdapterList
import androidx.ui.layout.Container
import androidx.ui.layout.LayoutPadding
import androidx.ui.layout.LayoutWidth
import androidx.ui.material.MaterialTheme
import androidx.ui.material.surface.Card
import androidx.ui.unit.dp

class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MaterialTheme {
        val data = 0.until(100).toList()
        AdapterList(data = data) {
          Card(modifier = LayoutPadding(top = 8.dp, bottom = 8.dp, left = 16.dp, right = 16.dp)) {
            Container(modifier = LayoutWidth.Fill + LayoutPadding(16.dp)) {
              Text("Hello ${it}")
            }
          }
        }
      }
    }
  }
}

f:id:sys1yagi:20200226163112p:plain:w250

AdapterListはまだ実用できない

0.1.0-dev05の時点ではまだ2コミットしかないので、実用に足らないのは当然っちゃ当然かなと思います。 https://android.googlesource.com/platform/frameworks/support/+log/refs/heads/androidx-compose-release/ui/ui-foundation/src/main/java/androidx/ui/foundation/AdapterList.kt

具体的な問題としては 要素のクリックイベントが動作しない という点があります。例えば次のコードのButtonはうまく動作しません。

class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MaterialTheme {
        val data = 0.until(100).toList()
        AdapterList(data = data) { value ->
          Card(modifier = LayoutPadding(top = 8.dp, bottom = 8.dp, left = 16.dp, right = 16.dp)) {
            Container(modifier = LayoutWidth.Fill + LayoutPadding(16.dp)) {
              Button(onClick = {
                println("click! $value")
              }) {
                Text("button ${value}")
              }
            }
          }
        }
      }
    }
  }
}

いくらか実験してみたところ、Ripple要素があるとクリックイベントが実行されないようです。Buttonは Ripple + Clickableで構成されているので反応しなくなっているようです。Rippleを使わずClickableのみを使えば動作はするのですが、タッチフィードバックがなくなるので厳しいです。

もちろんこうした問題は今後どんどん改善されていくと思いますが、今すぐに使うというのはちょっと難しそうです。

おわりに

ついにAdapterListが登場して実用段階への光が射してきましたね。さすがにプロダクション投入はまだまだ難しいですが、一部のViewをComposeに置き換えるのは十分できそうだなぁと思います。

Jetpack Compose 0.1.0-dev04から0.1.0-dev05にしたときに変更が必要だったところ

Jetpack Compose 0.1.0-dev05が出ました。 リリースノートはこちら https://developer.android.com/jetpack/androidx/releases/compose#0.1.0-dev05

前回に引き続き、アップデートでエラーになる部分と対応方法を紹介していきます。

androidx.ui.layout.Paddingが廃止

androidx.ui.layout.Paddingが廃止され、androidx.ui.layout.LayoutPaddingになりました。PaddingはComposableだったのに対して、LayoutPaddingはModifierです。次のような使い方になります。

before

Padding( 8.dp ) {
    Text(text = "こんにちは")
}

after

Text(
    modifier = LayoutPadding( 8.dp ),
    text = "こんにちは"
)

FontFamilyのコンストラクタがprivateになった

カスタムフォントを使う時はres/font にフォントファイルを置きつつ、FontFamilyを作って利用する形だったのですが、FontFamilyのコンストラクタがprivateになり、作り方が変わりました。

before

FontFamily(
  // res/font/ipam.ttf にファイルを置いておく
  Font(name = "ipam.ttf", weight = FontWeight.W400, style = FontStyle.Normal)
)

after

font(R.font.ipam, weight = FontWeight.W400, style = FontStyle.Normal)

Ambient.ofが廃止

Ambient.ofの代わりにambientOf関数が生えました。Providerも独立して、Providersになりました。Provideする値増えたらネスト大変だなーと思ってたところなのでちょうどいいです。

before

val IpamFontAmbient = Ambient.of<FontFamily>()

@Composable
fun IpamFontProvider(children : @Composable() () -> Unit) {
    val fontFamily = remember {
        FontFamily(
            Font(name = "ipam.ttf", weight = FontWeight.W400, style = FontStyle.Normal)
        )
    }
    IpamFontAmbient.Provider(value = fontFamily, children = children)
}

after

val IpamFontAmbient = ambientOf<Font>()

@Composable
fun IpamFontProvider(children : @Composable() () -> Unit) {
    val font = remember {
        font(R.font.ipam, weight = FontWeight.W400, style = FontStyle.Normal)
    }
    Providers(IpamFontAmbient.provides(value = font), children = children)
}

ambient関数が非推奨

上位のComposableからProvideされる値を取り出す時はambient関数を使ってましたが、非推奨になりました。代わりにambient関数にkeyとして渡しているAmbilentインスタンスcurrentを使います。

before

val font = ambient(IpamFontAmbient)

after

val font = IpamFontAmbient.current

androidx.ui.core.ambientDensity関数が廃止

Composable内でdensityを取り出せるandroidx.ui.core.ambientDensity関数がなくなりました。

before

import androidx.ui.core.ambientDensity

val density = ambientDensity()

after

import androidx.ui.core.DensityAmbient

val density = DensityAmbient.current

withDensity関数が廃止

withDensity関数は、DensityScope.() -> Unitを受け取ることで、Dp.toPx関数などの拡張関数を使えるスコープを提供するのですが、なくなりました。代わりにDensityScopeと同様の拡張関数を持つDensity interface が用意されました。DensityAmbient.currentでDensityインスタンスを取り出せるので、kotlinのwith関数を使って同じことができます。

before

withDensity(density) {
    Paint().apply {
        isAntiAlias = true
        style = PaintingStyle.stroke
        this.strokeWidth = strokeWidth.toPx().value
    }
}

after

val density = DensityAmbient.current

with(density) {
    Paint().apply {
        isAntiAlias = true
        style = PaintingStyle.stroke
        this.strokeWidth = strokeWidth.toPx().value
    }
}

ImageがPainterに変更

画像の表示はImageを使ってましたがPainterという抽象クラスが使われるようになりました。VectorAssetはImageになれないので、Painterで抽象化するのかなと思ったらVectorPainterはまだないみたいです。

before

AppBarIcon(
  icon = imageResource(R.drawable.ic_baseline_arrow_back_24),
  onClick = {
    backStack?.pop()
  }
)

after

AppBarIcon(
  icon = ImagePainter(imageResource(R.drawable.ic_baseline_arrow_back_24)),
  onClick = {
    backStack?.pop()
  }
)

Buttonの引数が変更

styleが展開されたほか、textがなくなりchildrenになりました。

before

Button(
    "キャンセル",
    style = ButtonStyle(
        backgroundColor = MaterialTheme.colors().secondary,
        contentColor = MaterialTheme.colors().onSecondary,
        shape = MaterialTheme.shapes().button,
        elevation = 2.dp
    ),
    onClick = onCloseRequest
)

after

Button(
    backgroundColor = MaterialTheme.colors().secondary,
    contentColor = MaterialTheme.colors().onSecondary,
    shape = MaterialTheme.shapes().button,
    elevation = 2.dp,
    onClick = onCloseRequest
) {
    Text("キャンセル")
}

終わりに

結構大変だった...。でもだいぶ洗練されてきた印象がある。今後が楽しみです。

Jetpack Compose (0.1.0-dev04) でSeekBarをスクラッチする

Jetpack ComposeにはSeekBarがないので、必要な場合は今の所自分で作ることになります。で作りました。0.1.0-dev04 での実装なので将来そのままでは動かなくなると思うのでご注意ください。

f:id:sys1yagi:20200209145316p:plain
Preview

使う

実際の動作はこんな感じになります

streamable.com

実装

Draggableを使って実装します。横棒とか丸は頑張って描画してます。 Draggableは値の範囲がfixedなので、横幅が動的(いわゆるmatch_parent)の場合利用が難しいです。そのためDraw関数とparentSizeを使って、widthをstateに持つみたいなことをやってます。

@Composable
private fun paint(): Paint {
    return Paint().apply {
        color = MaterialTheme.colors().primary
        isAntiAlias = true
    }
}

@Composable
fun SeekBar(
    @FloatRange(from = 0.0, to = 1.0) progress: Float,
    fixedWidth: Dp? = null,
    onChangeProgress: (Float) -> Unit
) {
    val squareSize = 32.dp
    val barHeight = 8.dp
    val fixedWidthPx = withDensity(ambientDensity()) { fixedWidth?.toPx()?.value }
    val (width, setWidth) = state {
        fixedWidthPx ?: 0f
    }
    if (width == 0f) {
        Container(
            modifier = LayoutWidth.Fill
        ) {
            Draw { _, parentSize ->
                val newWidth = parentSize.width.value
                if (newWidth != width) {
                    setWidth(newWidth)
                }
            }
        }
    } else {
        val squareSizePx = withDensity(ambientDensity()) { squareSize.toPx().value }

        val max = width - squareSizePx
        val min = 0.dp
        val (minPx, maxPx) = withDensity(ambientDensity()) {
            min.toPx().value to max
        }
        val position = animatedDragValue(maxPx * progress, minPx, maxPx)
        val paint = paint()

        Draggable(
            dragDirection = DragDirection.Horizontal,
            dragValue = position,
            onDragValueChangeRequested = {
                position.animatedFloat.snapTo(it)
                onChangeProgress(position.value / max)
            }
        ) {
            Container(
                modifier = fixedWidth?.let { LayoutWidth(it) } ?: LayoutWidth.Fill,
                alignment = Alignment.CenterLeft,
                height = squareSize
            ) {
                Stack {
                    Padding(
                        top = squareSize / 2 - barHeight / 2,
                        left = squareSize / 2,
                        right = squareSize / 2
                    ) {
                        ColoredRect(
                            Color.LightGray,
                            height = barHeight
                        )
                    }
                    Draw { canvas, _ ->
                        canvas.drawCircle(
                            Offset(position.value + squareSizePx / 2, squareSizePx / 2),
                            squareSizePx / 2,
                            paint
                        )
                    }
                }
            }
        }
    }
}

おわりに

ProgressBarなども横幅fixedなんでスクラッチしたり、わりとスクラッチが必要だけど、結構カスタムなコンポーネント作るのそんなに難しくないので、UIライブラリがどんどん出てくるかもなと思ったりします。β、RCが待ち遠しいですね。

Jetpack Compose 0.1.0-dev03から0.1.0-dev04にしたときに変更が必要だったところ

Jetpack Compose 0.1.0-dev04が出ましたね、今回からリリースノートのページもできたみたいです。

developer.android.com

まだプレビューなんでどんどんAPIが変わっていきます。もしまともに使ってるとえらいことになるわけですが、個人的にproduction readyを待たずなんかアプリ出したろと思っているのでガンガン使っています。 で、0.1.0-dev03から0.1.0-dev04にしてみると案の定えらいことになったので変更が必要だったところをまとめます。

f:id:sys1yagi:20200131084046j:plain
0.1.0-dev03から0.1.0-dev04にしたときの様子

コンパイラの設定

0.1.0-dev03では、どうもkaptとの相性が悪く、Backend Internal error: Exception during code generation みたいなエラーがでてコンパイルできなかったのですが、0.1.0-dev04では、オプションを追加することで回避できるようになったようです。

android {
  composeOptions {
    kotlinCompilerExtensionVersion "0.1.0-dev04"
  }
}

最初、compileOptionsに書いてエラーになって頭を抱えたんですが、composeOptionsでした。

unaryPlusの廃止

+state とか +ambient とか +imageResource とかの、+が要らなくなりました。単純に+を消して回ればOK。

effectOfが廃止

unaryPlusの廃止と同時にeffectOfも廃止になりました。代わりに @Compose を使えとのこと。

before

private fun paint(color: Color, strokeCap: StrokeCap, strokeWidth: Dp) = effectOf<Paint> {
    val paint = +memo { Paint() }
    // ...
    paint
}

after

@Composable
private fun paint(color: Color, strokeCap: StrokeCap, strokeWidth: Dp): Paint {
    val paint = remember { Paint() }
    // ...
    return paint
}

まぁカスタムでeffectOf使うケースあんまりなさそうなのでもし引っかかったらという感じです。

memoがrememberにリネーム

before

val count = +memo { 0 }

after

val count = remember { 0 }

dp, sp, IntPxなどが移動

before

import androidx.ui.core.Dp
import androidx.ui.core.PxSize
import androidx.ui.core.dp
import androidx.ui.core.sp

after

import androidx.ui.unit.Dp
import androidx.ui.unit.PxSize
import androidx.ui.unit.dp
import androidx.ui.unit.sp

FlexRow, FlexColumnが非推奨

ここが一番たいへんでした。FlexRow、FlexColumnが非推奨となり、代わりにRow、Columnを使えとのこと。

次のようなレイアウトを考えると、

f:id:sys1yagi:20200131092353p:plain

以前はFlexRowとinflexible, flexibleを使って書いてました。

before

@Preview
@Composable
fun DefaultPreview() {
  MaterialTheme {
    FlexRow {
      inflexible {
        Padding(16.dp) {
          Row {
            Padding(left = 4.dp) {
              Text("1")
            }
            Padding(left = 4.dp) {
              Text("2")
            }
          }
        }
      }
      flexible(1f) {
        Container(
          modifier = ExpandedWidth,
          alignment = Alignment.TopRight
        ) {
          Padding(16.dp) {
            Text("こんにちは")
          }
        }
      }
    }
  }
}

FlexRowが非推奨となりinflexible, flexibleなども消滅しました。代わりにRowを使います。

after

@Preview
@Composable
fun DefaultPreview() {
  MaterialTheme {
    Row(modifier = LayoutWidth.Fill) {
      Padding(16.dp) {
        Row {
          Padding(left = 4.dp) {
            Text("1")
          }
          Padding(left = 4.dp) {
            Text("2")
          }
        }
      }
      Container(
        alignment = Alignment.TopRight,
        modifier = LayoutFlexible(1f)
      ) {
        Padding(16.dp) {
          Text("こんにちは")
        }
      }
    }
  }
}

Rowの中はデフォルトがinflexibleです。flexibleはLayoutFlexibleを使います。LayoutFlexibleRowScopeからしかアクセスできません。

ExpandHeight, ExpandWidthの廃止

LayoutHeight.Fill, LayoutWidth.Fillになりました。

おわり

どんどん進化してますね。βが出るのが楽しみです。ScrollingListというRecyclerViewぽいやつも早くほしいっす。