Open18

AndroidStudioの物置

pecopeco

このスクラップについて

何度も書くことになりそうな書き方を個人的なメモ代わりに置いておくための場所です
個人用ではありますが、自由にコピペとかして使ってください
ただ動作保証とかはできません
marerial3です

pecopeco

clickableなUIコンポーネント

JetpackComposeのコンポーネントのクリック周りをカスタムしたもの

pecopeco

Box

クリックやロングクリックに対応したBox

// クリック可能
@Suppress("unused")
@Composable
inline fun Box(
    noinline onClick: () -> Unit,
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content: @Composable BoxScope.() -> Unit
) {
    androidx.compose.foundation.layout.Box(
        modifier = modifier
            .clickable(
                onClick = onClick
            ),
        contentAlignment = contentAlignment,
        propagateMinConstraints = propagateMinConstraints,
        content = content
    )
}

// クリック+ロングクリック可能
@Suppress("unused")
@OptIn(ExperimentalFoundationApi::class)
@Composable
inline fun Box(
    modifier: Modifier = Modifier,
    noinline onClick: (() -> Unit)? = null,
    noinline onLongClick: () -> Unit,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content: @Composable BoxScope.() -> Unit
) {
    androidx.compose.foundation.layout.Box(
        modifier = modifier
            .combinedClickable(
                onClick = onClick?: { },
                onLongClick = onLongClick
            ),

        contentAlignment = contentAlignment,
        propagateMinConstraints = propagateMinConstraints,
        content = content
    )
}

// 中身なし
// クリック可能
@Suppress("unused")
@Composable
fun Box(
    onClick: () -> Unit,
    modifier: Modifier
) {
    androidx.compose.foundation.layout.Box(
        modifier = modifier
            .clickable (
                onClick = onClick
            )
        )
}

// 中身なし
// クリック+ロングクリック可能
@Suppress("unused")
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Box(
    onClick: (() -> Unit)? = null,
    onLongClick: () -> Unit,
    modifier: Modifier
) {
    androidx.compose.foundation.layout.Box(
        modifier = modifier
            .combinedClickable(
                onClick = onClick?: { },
                onLongClick = onLongClick
            ),

    )
}

pecopeco

Column

クリックやロングクリックに対応したColumn

// クリック可能
@Suppress("unused")
@Composable
inline fun Column(
    noinline onClick: () -> Unit,
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
) {
    androidx.compose.foundation.layout.Column(
        modifier = modifier
            .clickable(
                onClick = onClick
            ),

        verticalArrangement = verticalArrangement,
        horizontalAlignment = horizontalAlignment,
        content = content
    )
}

// クリック+ロングクリック可能
@Suppress("unused")
@OptIn(ExperimentalFoundationApi::class)
@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    noinline onClick: (() -> Unit)? = null,
    noinline onLongClick: () -> Unit,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
) {
    androidx.compose.foundation.layout.Column(
        modifier = modifier
            .combinedClickable(
                onClick = onClick?: { },
                onLongClick = onLongClick
            ),

        verticalArrangement = verticalArrangement,
        horizontalAlignment = horizontalAlignment,
        content = content
    )
}
pecopeco

Row

クリックやロングクリックに対応したRow

// クリック可能
@Suppress("unused")
@Composable
inline fun Row(
    noinline onClick: () -> Unit,
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    content: @Composable RowScope.() -> Unit
) {
    androidx.compose.foundation.layout.Row(
        modifier = modifier
            .clickable(
                onClick = onClick
            ),

        horizontalArrangement = horizontalArrangement,
        verticalAlignment = verticalAlignment,
        content = content
    )
}

// クリック+ロングクリック可能
@Suppress("unused")
@OptIn(ExperimentalFoundationApi::class)
@Composable
inline fun Row(
    modifier: Modifier = Modifier,
    noinline onClick: (() -> Unit)? = null,
    noinline onLongClick: () -> Unit,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    content: @Composable RowScope.() -> Unit
) {
    androidx.compose.foundation.layout.Row(
        modifier = modifier
            .combinedClickable(
                onClick = onClick?: { },
                onLongClick = onLongClick
            ),
        horizontalArrangement = horizontalArrangement,
        verticalAlignment = verticalAlignment,
        content = content
    )
}
pecopeco

Button

クリックやロングクリックに対応したButton

// ロングクリック可能
@Suppress("unused")
@Composable
fun Button(
    modifier: Modifier = Modifier,
    onClick: (() -> Unit)? = null,
    onLongClick: () -> Unit,
    enabled: Boolean = true,
    shape: Shape = ButtonDefaults.shape,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    // elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
    border: BorderStroke? = null,
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    content: @Composable RowScope.() -> Unit
) {
    val containerColor = if (enabled) colors.containerColor else colors.disabledContainerColor
    val contentColor = if (enabled) colors.contentColor else colors.disabledContentColor
    val shadowElevation = 0.0.dp // elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp
    val tonalElevation = 0.0.dp //elevation?.tonalElevation(enabled) ?: 0.dp

    Surface(
        onClick = onClick,
        onLongClick = onLongClick,
        modifier = modifier.semantics { role = Role.Button },
        enabled = enabled,
        shape = shape,
        color = containerColor,
        contentColor = contentColor,
        tonalElevation = tonalElevation,
        shadowElevation = shadowElevation,
        interactionSource = interactionSource,
        border = border
    ) {
        ProvideContentColorTextStyle(
            contentColor = contentColor,
            textStyle = MaterialTheme.typography.labelLarge) {
            androidx.compose.foundation.layout.Row(
                Modifier
                    .defaultMinSize(
                        minWidth = ButtonDefaults.MinWidth,
                        minHeight = ButtonDefaults.MinHeight
                    )
                    .padding(contentPadding),
                horizontalArrangement = Arrangement.Center,
                verticalAlignment = Alignment.CenterVertically,
                content = content
            )
        }
    }
}

// ロングクリック可能
@Suppress("unused")
@Composable
fun FloatingActionButton(
    modifier: Modifier = Modifier,
    onClick: (() -> Unit)? = null,
    onLongClick: () -> Unit,
    shape: Shape = FloatingActionButtonDefaults.shape,
    containerColor: Color = FloatingActionButtonDefaults.containerColor,
    contentColor: Color = contentColorFor(containerColor),
    // elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    content: @Composable () -> Unit,
) {
    val tonalElevation = 0.0.dp  // elevation.tonalElevation() ?: 0.dp
    val shadowElevation = 0.0.dp //elevation.shadowElevation(interactionSource = interactionSource).value 0.dp

    Surface(
        onClick = onClick,
        onLongClick = onLongClick,
        modifier = modifier.semantics { role = Role.Button },
        shape = shape,
        color = containerColor,
        contentColor = contentColor,
        tonalElevation = tonalElevation,
        shadowElevation = shadowElevation,
        interactionSource = interactionSource,
    ) {
        ProvideContentColorTextStyle(
            contentColor = contentColor,
            textStyle = MaterialTheme.typography.labelLarge
        ) {
            androidx.compose.foundation.layout.Box(
                modifier = Modifier
                    .defaultMinSize(
                        minWidth = 56.0.dp,
                        minHeight = 56.0.dp,
                    ),
                contentAlignment = Alignment.Center,
            ) { content() }
        }
    }
}

/*
 * ロングクリック可能なボタン用のSurface
 * internalな関連関数もコピー
 */
@OptIn(ExperimentalFoundationApi::class)
@Composable
@NonRestartableComposable
private fun Surface(
    modifier: Modifier = Modifier,
    onClick: (() -> Unit)? = null,
    onLongClick: () -> Unit,
    enabled: Boolean = true,
    shape: Shape = RectangleShape,
    color: Color = MaterialTheme.colorScheme.surface,
    contentColor: Color = contentColorFor(color),
    tonalElevation: Dp = 0.0.dp,
    shadowElevation: Dp = 0.0.dp,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    indication: Indication? = rememberRipple(),
    border: BorderStroke? = null,
    content: @Composable () -> Unit
) {
    val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
    CompositionLocalProvider(
        LocalContentColor provides contentColor,
        LocalAbsoluteTonalElevation provides absoluteElevation
    ) {
        @Suppress("DEPRECATION_ERROR")
        (androidx.compose.foundation.layout.Box(
            modifier = modifier
                .minimumInteractiveComponentSize()
                .surface(
                    shape = shape,
                    backgroundColor = surfaceColorAtElevation(
                        color = color,
                        elevation = absoluteElevation
                    ),
                    border = border,
                    shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() }
                )
                .combinedClickable(
                    enabled = enabled,
                    onClick = onClick?: { },
                    onLongClick = onLongClick,
                    indication = indication,
                    interactionSource = interactionSource,
                ),
            propagateMinConstraints = true
        ) {
            content()
        })
    }
}

@Stable
private fun Modifier.surface(
    shape: Shape,
    backgroundColor: Color,
    border: BorderStroke?,
    shadowElevation: Float,
) = this
    .graphicsLayer(shadowElevation = shadowElevation, shape = shape, clip = false)
    .then(if (border != null) Modifier.border(border, shape) else Modifier)
    .background(color = backgroundColor, shape = shape)
    .clip(shape)

@Composable
private fun surfaceColorAtElevation(color: Color, elevation: Dp): Color =
    MaterialTheme.colorScheme.applyTonalElevation(color, elevation)

@Composable
@ReadOnlyComposable
private fun ColorScheme.applyTonalElevation(backgroundColor: Color, elevation: Dp): Color {
    val tonalElevationEnabled = LocalTonalElevationEnabled.current
    return if (backgroundColor == surface && tonalElevationEnabled) {
        surfaceColorAtElevation(elevation)
    } else {
        backgroundColor
    }
}

@Composable
private fun ProvideContentColorTextStyle(
    contentColor: Color,
    textStyle: TextStyle,
    content: @Composable () -> Unit
) {
    val mergedStyle = LocalTextStyle.current.merge(textStyle)
    CompositionLocalProvider(
        LocalContentColor provides contentColor,
        LocalTextStyle provides mergedStyle,
        content = content
    )
}
pecopeco

拡張関数

pecopeco

JSON

org.jsonの拡張関数

// JsonArrayをforeachで扱う
@Suppress("unused")
inline fun JSONArray.forEachIndexOnly(action: (Int) -> Unit) {
    for (index in 0 until length()) action(index)
}

// JsonArrayをmapで扱う
@Suppress("unused")
inline fun <R> JSONArray.mapIndexOnly(action: (Int) -> R): List<R> {
    return (0 until length()).map { index -> action(index) }
}
pecopeco

Collection

Collection,Iterableの拡張関数

// ArrayListに変換
@Suppress("unused")
fun <T> Iterable<T>.toArrayList(): ArrayList<T> {
    if (this is Collection<T>)
        return this.toArrayList()
    return toCollection(ArrayList<T>())
}

// ArrayListに変換
@Suppress("unused")
fun <T> Collection<T>.toArrayList(): ArrayList<T> {
    return ArrayList(this)
}
pecopeco

ActivityResult関連

rememberLauncherForActivityResultを拡張したもの

pecopeco

Permission

ActivityResultContracts.RequestPermissionを拡張したもの
権限要求を行い、許可された,されているかを確認する

input

  • permission 要求する権限
  • onGrant 権限が許可された,されていた際に行う処理

output

  • launch() 権限要求を実行する
  • isGranted() 権限が有効かどうかを取得
@Stable
data class PermissionResultLauncher(
    private val context: Context,
    private val permission: String,
    private val launcher: ManagedActivityResultLauncher<String, Boolean>,
    private val onAlreadyGranted: () -> Unit
) {
    fun launch() {
        if (!isGranted()) {
            launcher.launch(permission)
        } else {
            onAlreadyGranted()
        }
    }
    fun isGranted(): Boolean {
        return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
    }
}

@Composable
fun rememberParmissionResult(
    permission: String,
    onGrant: () -> Unit
): PermissionResultLauncher {
    val context = LocalContext.current
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission()
    ) { isGrant ->
        if (isGrant) {
            onGrant()
        }
    }
    return remember {
        PermissionResultLauncher(
            context = context,
            permission = permission,
            launcher = launcher,
            onAlreadyGranted = onGrant
        )
    }
}
pecopeco

WiFi SettingPanel

ActivityResultContracts.StartActivityForResult()を拡張したもの
WiFiのSettingPanelを呼び出し、有効化された,されていたかを確認する
Api29未満ではWi-Fiを有効化する

input

  • onEnable WiFiが有効化された,されていた場合に行う処理

output

  • launch() SettingPanelを呼び出す
  • isEnabled() WiFiが有効化されているかを取得
@Stable
data class WifiResultLauncher(
    private val wifiManager: WifiManager,
    private val launcher: ManagedActivityResultLauncher<Intent, ActivityResult>,
    private val onAlreadyEnabled: () -> Unit
) {
    @RequiresApi(Build.VERSION_CODES.Q)
    private val intent = Intent(Settings.Panel.ACTION_WIFI)
    fun launch() {
        if (!isEnabled()) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                launcher.launch(intent)
            }
            else {
                @Suppress("DEPRECATION")
                wifiManager.isWifiEnabled = true
            }
        } else {
            onAlreadyEnabled()
        }
    }
    fun isEnabled(): Boolean {
        return wifiManager.isWifiEnabled()
    }
}

@Composable
fun rememberWifiResult(
    onEnable: () -> Unit
): WifiResultLauncher {
    val wifiManager =
        LocalContext.current.applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.StartActivityForResult()
    ) {
        if (wifiManager.isWifiEnabled()) {
            onEnable()
        }
    }
    return remember {
        WifiResultLauncher(
            wifiManager = wifiManager,
            launcher = launcher,
            onAlreadyEnabled = onEnable
        )
    }
}
pecopeco

Animation関連

pecopeco

AnimatedVisibility

アニメーションが設定された状態で呼び出されるAnimatedVisibility
このコードではfadeIn(),fadeOut()

@Suppress("unused")
@Composable
fun FadeInAnimated(
    visible: Boolean,
    modifier: Modifier = Modifier,
    label: String = "AnimatedVisibility",
    content: @Composable() (AnimatedVisibilityScope.() -> Unit)
) {
    val enter: EnterTransition = fadeIn()
    val exit: ExitTransition = fadeOut()
    AnimatedVisibility(
        visible = visible,
        enter = enter,
        exit = exit,
        modifier = modifier,
        label = label,
        content = content
    )
}
pecopeco

AnimatedContent

AnimatedContentを拡張したもので、基本的にAnimatedVisibilityのように動作する
visiblefalseの際、space分のスペースが確保される
また、アニメーションが設定された状態で呼び出せされる
このコードではfadeIn(),fadeOut()

@Composable
fun FadeInSpaceableAnimated(
    visible: Boolean,
    space: Dp,
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    label: String = "AnimatedContent",
    contentKey: (targetState: Boolean) -> Any? = { it },
    content: @Composable() AnimatedContentScope.() -> Unit
) {
    val transitionSpec: AnimatedContentTransitionScope<Boolean>.() -> ContentTransform = {
        (fadeIn(animationSpec = tween(300, delayMillis = 90)))
            .togetherWith(fadeOut(animationSpec = tween(90)))
    }
    AnimatedContent(
        targetState = visible,
        modifier = modifier,
        transitionSpec = transitionSpec,
        contentAlignment = contentAlignment,
        label = label,
        contentKey = contentKey
    ) { isVisible ->
        if (isVisible) {
            content()
        } else {
            Spacer(modifier = Modifier.size(space))
        }
    }
}
pecopeco

LazyList関連

JetpackComposeのLazyList,LazyColumn,LazyRow,などをカスタムしたもの

pecopeco

animateItem()

animateItem()が各アイテムに設定された状態で呼び出せるLazyListitems拡張

@Suppress("unused")
inline fun <T> LazyListScope.animatedItems(
  items: List<T>,
  noinline key: ((item: T) -> Any)? = null,
  noinline contentType: (item: T) -> Any? = { null },
  crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
) = items(
  count = items.size,
  key = if (key != null) { index: Int -> key(items[index]) } else null,
  contentType = { index: Int -> contentType(items[index]) }
) { index ->
  Box(modifier = Modifier.animateItem()) {
      itemContent(items[index])
  }
}
pecopeco

LazyPager

Pagerっぽく動作させられるLazyList
Listを直接渡せる
このコードではHorizontalPagerに近い感じ

@Suppress("unused")
@Composable
fun LazyHorizontalPager(
    modifier: Modifier = Modifier,
    state: LazyListState = rememberLazyListState(),
    contentPadding: PaddingValues = PaddingValues(all = 0.dp),
    reverseLayout: Boolean = false,
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    content: LazyListScope.() -> Unit
) {
    val flingBehavior = rememberLazyPagerFlingBehavior(state = state)

    androidx.compose.foundation.lazy.LazyRow(
        modifier = modifier,
        state = state,
        contentPadding = contentPadding,
        reverseLayout = reverseLayout,
        verticalAlignment = verticalAlignment,
        horizontalArrangement = Arrangement.spacedBy(10.dp), // 画面外に余白を作って一つだけcompositionされるように
        flingBehavior = flingBehavior,
        userScrollEnabled = true,
        content = content
    )
}

// PagerのっぽくスクロールさせるためのFlingBehavior
@Composable
fun rememberLazyPagerFlingBehavior(
    state: LazyListState,
    decayAnimationSpec: DecayAnimationSpec<Float> = remember {
        exponentialDecay(frictionMultiplier = 20.0f) // スワイプのブレーキ
    },
    snapAnimationSpec: AnimationSpec<Float> = spring(
        stiffness = Spring.StiffnessLow,
        visibilityThreshold = Int.VisibilityThreshold.toFloat()
    ),
): FlingBehavior {
    val density = LocalDensity.current
    val layoutDirection = LocalLayoutDirection.current

    return remember(state, decayAnimationSpec, snapAnimationSpec, density, layoutDirection) {
        val snapLayoutInfoProvider = SnapLayoutInfoProvider(
            lazyListState = state,
            snapPosition = SnapPosition.Start
        )

        snapFlingBehavior(
            snapLayoutInfoProvider = snapLayoutInfoProvider,
            decayAnimationSpec = decayAnimationSpec,
            snapAnimationSpec = snapAnimationSpec
        )
    }
}

// Pagerっぽく表示させるためのitems
@Suppress("unused")
inline fun <T> LazyListScope.animatedPagerItems(
    items: List<T>,
    noinline key: ((item: T) -> Any)? = null,
    noinline contentType: (item: T) -> Any? = { null },
    crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
) = items(
    count = items.size,
    key = if (key != null) { index: Int -> key(items[index]) } else null,
    contentType = { index: Int -> contentType(items[index]) }
) { index ->
    androidx.compose.foundation.layout.Box(
        modifier = Modifier
            .animateItem()
            .fillParentMaxSize() // 一つずつ表示
    ) {
        itemContent(items[index])
    }
}