สถานะและภาพเคลื่อนไหวในสไตล์

Styles API มีแนวทางที่ประกาศและปรับปรุงแล้วในการจัดการการเปลี่ยนแปลง UI ในระหว่างสถานะการโต้ตอบ เช่น hovered, focused และ pressed API นี้ช่วยลดโค้ดมาตรฐานที่มักจะต้องใช้เมื่อใช้ตัวแก้ไขได้อย่างมาก

StyleState ทำหน้าที่เป็นอินเทอร์เฟซแบบอ่านอย่างเดียวที่เสถียร ซึ่งติดตามสถานะที่ใช้งานอยู่ขององค์ประกอบ (เช่น สถานะที่เปิดใช้ กด หรือโฟกัส) เพื่ออำนวยความสะดวกในการจัดรูปแบบแบบรีแอ็กทีฟ ภายใน StyleScope คุณสามารถเข้าถึงได้ผ่านพร็อพเพอร์ตี้ state เพื่อใช้ตรรกะแบบมีเงื่อนไขในการกำหนดสไตล์โดยตรง

การโต้ตอบตามสถานะ: วางเมาส์ โฟกัส กด เลือก เปิดใช้ สลับ

สไตล์มาพร้อมกับการรองรับการโต้ตอบทั่วไปในตัว ดังนี้

  • กดแล้ว
  • วางเมาส์
  • เลือกแล้ว
  • เปิดใช้อยู่
  • สลับแล้ว

นอกจากนี้ ยังรองรับสถานะที่กำหนดเองได้ด้วย ดูข้อมูลเพิ่มเติมได้ที่ส่วนการจัดรูปแบบสถานะที่กำหนดเองด้วย StyleState

จัดการสถานะการโต้ตอบด้วยพารามิเตอร์สไตล์

ตัวอย่างต่อไปนี้แสดงการแก้ไข background และ borderColor เพื่อตอบสนองต่อสถานะการโต้ตอบ โดยเฉพาะการเปลี่ยนเป็นสีม่วงเมื่อวางเมาส์เหนือองค์ประกอบ และสีน้ำเงินเมื่อโฟกัส:

@Preview
@Composable
private fun OpenButton() {
    BaseButton(
        style = outlinedButtonStyle then {
            background(Color.White)
            hovered {
                background(lightPurple)
                border(2.dp, lightPurple)
            }
            focused {
                background(lightBlue)
            }
        },
        onClick = {  },
        content = {
            BaseText("Open in Studio", style = {
                contentColor(Color.Black)
                fontSize(26.sp)
                textAlign(TextAlign.Center)
            })
        }
    )
}

รูปที่ 1 เปลี่ยนสีพื้นหลังตามสถานะเมื่อวางเมาส์และโฟกัส

นอกจากนี้ คุณยังสร้างคำจำกัดความสถานะที่ซ้อนกันได้ด้วย เช่น คุณสามารถกำหนด รูปแบบที่เฉพาะเจาะจงสำหรับเมื่อมีการกดปุ่มและวางเมาส์เหนือปุ่ม พร้อมกันได้ดังนี้

@Composable
private fun OpenButton_CombinedStates() {
    BaseButton(
        style = outlinedButtonStyle then {
            background(Color.White)
            hovered {
                // light purple
                background(lightPurple)
                pressed {
                    // When running on a device that can hover, whilst hovering and then pressing the button this would be invoked
                    background(lightOrange)
                }
            }
            pressed {
                // when running on a device without a mouse attached, this would be invoked as you wouldn't be in a hovered state only
                background(lightRed)
            }
            focused {
                background(lightBlue)
            }
        },
        onClick = {  },
        content = {
            BaseText("Open in Studio", style = {
                contentColor(Color.Black)
                fontSize(26.sp)
                textAlign(TextAlign.Center)
            })
        }
    )
}

รูปที่ 2 สถานะวางเมาส์และสถานะกดบนปุ่ม

Composable ที่กำหนดเองด้วย Modifier.styleable

เมื่อสร้างstyleableคอมโพเนนต์ของคุณเอง คุณต้องเชื่อมต่อ interactionSource กับ styleState จากนั้นส่งสถานะนี้ไปยัง Modifier.styleable เพื่อใช้งาน

พิจารณาสถานการณ์ที่ระบบการออกแบบมี GradientButton คุณอาจต้องการสร้าง LoginButton ที่รับค่าจาก GradientButton แต่เปลี่ยนสีระหว่างการโต้ตอบ เช่น เมื่อมีการกด

  • หากต้องการเปิดใช้การอัปเดตสไตล์ interactionSource ให้ใส่ interactionSource เป็นพารามิเตอร์ภายใน Composable ใช้พารามิเตอร์ที่ระบุ หรือหากไม่มี ให้เริ่มต้น MutableInteractionSource ใหม่
  • เริ่มต้น styleState โดยระบุ interactionSource ตรวจสอบว่าสถานะที่เปิดใช้ของ styleState สะท้อนค่าของพารามิเตอร์ที่เปิดใช้ที่ระบุ
  • กำหนดตัวแก้ไข interactionSource ให้กับตัวแก้ไข focusable และ clickable สุดท้าย ให้ใช้ styleState กับพารามิเตอร์ styleable ของตัวแก้ไข

@Composable
private fun GradientButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    style: Style = Style,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource? = null,
    content: @Composable RowScope.() -> Unit,
) {
    val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
    val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
    styleState.isEnabled = enabled
    Row(
        modifier =
            modifier
                .clickable(
                    onClick = onClick,
                    enabled = enabled,
                    interactionSource = interactionSource,
                    indication = null,
                )
                .styleable(styleState, baseGradientButtonStyle then style),
        content = content,
    )
}

ตอนนี้คุณใช้interactionSource state เพื่อขับเคลื่อนการแก้ไขสไตล์ด้วย ตัวเลือก pressed, focused และ hovered ภายในบล็อกสไตล์ได้แล้ว

@Preview
@Composable
fun LoginButton() {
    val loginButtonStyle = Style {
        pressed {
            background(
                Brush.linearGradient(
                    listOf(Color.Magenta, Color.Red)
                )
            )
        }
    }
    GradientButton(onClick = {
        // Login logic
    }, style = loginButtonStyle) {
        BaseText("Login")
    }
}

รูปที่ 3 การเปลี่ยนสถานะที่ประกอบกันได้ที่กำหนดเองตาม interactionSource

เปลี่ยนสไตล์ภาพเคลื่อนไหว

การเปลี่ยนแปลงสถานะของสไตล์มาพร้อมกับการรองรับภาพเคลื่อนไหวในตัว คุณสามารถรวมพร็อพเพอร์ตี้ใหม่ ไว้ในบล็อกการเปลี่ยนสถานะใดก็ได้ด้วย animate เพื่อเพิ่ม ภาพเคลื่อนไหวระหว่างสถานะต่างๆ โดยอัตโนมัติ ซึ่งคล้ายกับ animate*AsState API ตัวอย่างต่อไปนี้จะเคลื่อนไหว borderColor จากสีดำเป็นสีน้ำเงินเมื่อ สถานะเปลี่ยนเป็นโฟกัส

val animatingStyle = Style {
    externalPadding(48.dp)
    border(3.dp, Color.Black)
    background(Color.White)
    size(100.dp)

    pressed {
        animate {
            borderColor(Color.Magenta)
            background(Color(0xFFB39DDB))
        }
    }
}

@Preview
@Composable
private fun AnimatingStyleChanges() {
    val interactionSource = remember { MutableInteractionSource() }
    val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
    Box(modifier = Modifier
        .clickable(
            interactionSource,
            enabled = true,
            indication = null,
            onClick = {

            }
        )
        .styleable(styleState, animatingStyle)) {

    }
}

รูปที่ 4 เปลี่ยนสีเมื่อกด

animate API ยอมรับ animationSpec เพื่อเปลี่ยนระยะเวลาหรือรูปร่างของ เส้นโค้งภาพเคลื่อนไหว ตัวอย่างต่อไปนี้จะเคลื่อนไหวขนาดของกล่องด้วยspring spec

val animatingStyleSpec = Style {
    externalPadding(48.dp)
    border(3.dp, Color.Black)
    background(Color.White)
    size(100.dp)
    transformOrigin(TransformOrigin.Center)
    pressed {
        animate {
            borderColor(Color.Magenta)
            background(Color(0xFFB39DDB))
        }
        animate(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) {
            scale(1.2f)
        }
    }
}

@Preview(showBackground = true)
@Composable
fun AnimatingStyleChangesSpec() {
    val interactionSource = remember { MutableInteractionSource() }
    val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
    Box(modifier = Modifier
        .clickable(
            interactionSource,
            enabled = true,
            indication = null,
            onClick = {

            }
        )
        .styleable(styleState, animatingStyleSpec))
}

รูปที่ 5 การเปลี่ยนขนาดและสีเมื่อกด

การจัดรูปแบบสถานะที่กำหนดเองด้วย StyleState

คุณอาจมีสไตล์ที่แตกต่างกันซึ่ง ได้รับการสนับสนุนโดยสถานะที่กำหนดเอง ทั้งนี้ขึ้นอยู่กับกรณีการใช้งานที่ประกอบได้ ตัวอย่างเช่น หากคุณมีแอปสื่อ คุณอาจต้องการ ใช้สไตล์ที่แตกต่างกันสำหรับปุ่มใน MediaPlayer Composable ทั้งนี้ขึ้นอยู่กับสถานะการเล่นของเพลเยอร์ ทำตามขั้นตอนต่อไปนี้เพื่อสร้างและ ใช้สถานะที่กำหนดเอง

  1. กำหนดคีย์ที่กำหนดเอง
  2. สร้างส่วนขยาย StyleState
  3. ลิงก์ไปยังสถานะที่กำหนดเอง

กำหนดคีย์ที่กำหนดเอง

หากต้องการสร้างสไตล์ตามสถานะที่กำหนดเอง ให้สร้าง StyleStateKeyก่อน แล้วส่งค่าสถานะเริ่มต้น เมื่อแอปเปิดขึ้น มีเดียเพลเยอร์จะอยู่ในสถานะ Stopped จึงต้องเริ่มต้นใช้งานด้วยวิธีต่อไปนี้

enum class PlayerState {
    Stopped,
    Playing,
    Paused
}

val playerStateKey = StyleStateKey(PlayerState.Stopped)

สร้างฟังก์ชันส่วนขยาย StyleState

กำหนดฟังก์ชันส่วนขยายใน StyleState เพื่อค้นหา playState ปัจจุบัน จากนั้นสร้างฟังก์ชันส่วนขยายใน StyleScope โดยส่งสถานะที่กำหนดเองใน playStateKey, Lambda ที่มีสถานะเฉพาะ และสไตล์

// Extension Function on MutableStyleState to query and set the current playState
var MutableStyleState.playerState
    get() = this[playerStateKey]
    set(value) { this[playerStateKey] = value }

fun StyleScope.playerPlaying(value: Style) {
    state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing })
}
fun StyleScope.playerPaused(value: Style) {
    state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused })
}

กำหนด styleState ใน Composable และตั้งค่า styleState.playState ให้เท่ากับสถานะขาเข้า ส่ง styleState ไปยังฟังก์ชัน styleable ใน ตัวแก้ไข

ภายใน Lambda ของ style คุณสามารถใช้การจัดรูปแบบตามสถานะสำหรับสถานะที่กำหนดเองได้ โดยใช้ฟังก์ชันส่วนขยายที่กำหนดไว้ก่อนหน้านี้

โค้ดต่อไปนี้คือข้อมูลโค้ดแบบเต็มสำหรับตัวอย่างนี้

enum class PlayerState {
    Stopped,
    Playing,
    Paused
}
val playerStateKey = StyleStateKey<PlayerState>(PlayerState.Stopped)
var MutableStyleState.playerState
    get() = this[playerStateKey]
    set(value) { this[playerStateKey] = value }

fun StyleScope.playerPlaying(value: Style) {
    state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing })
}
fun StyleScope.playerPaused(value: Style) {
    state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused })

}

@Composable
fun MediaPlayer(
    url: String,
    modifier: Modifier = Modifier,
    style: Style = Style,
    state: PlayerState = remember { PlayerState.Paused }
) {
    // Hoist style state, set playstate as a parameter,
    val styleState = remember { MutableStyleState(null) }
    // Set equal to incoming state to link the two together
    styleState.playerState = state
    Box(
        modifier = modifier.styleable(styleState, Style {
            size(100.dp)
            border(2.dp, Color.Red)

        }, style, )) {

        ///..
    }
}
@Composable
fun StyleStateKeySample() {
    // Using the extension function to change the border color to green while playing
    val style = Style {
        borderColor(Color.Gray)
        playerPlaying {
            animate {
                borderColor(Color.Green)
            }
        }
        playerPaused {
            animate {
                borderColor(Color.Blue)
            }
        }
    }
    val styleState = remember { MutableStyleState(null) }
    styleState[playerStateKey] = PlayerState.Playing

    // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too.
    MediaPlayer(url = "https://example.com/media/video",
        style = style,
        state = PlayerState.Stopped)
}