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

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

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

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

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

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

นอกจากนี้ยังรองรับสถานะที่กำหนดเองได้ด้วย ดูข้อมูลเพิ่มเติมได้ที่ส่วนการจัดรูปแบบสถานะที่กำหนดเองด้วย Custom State Styling with 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 สถานะวางเมาส์เหนือและกดปุ่มพร้อมกัน

คอมโพสได้ที่กำหนดเองด้วย Modifier.styleable

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

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

  • หากต้องการเปิดใช้การอัปเดตสไตล์ interactionSource ให้ใส่ interactionSource เป็นพารามิเตอร์ภายในคอมโพสได้ ใช้พารามิเตอร์ที่ให้มา หรือหากไม่มี ให้เริ่มต้น 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 = rememberUpdatedStyleState(interactionSource) {
        it.isEnabled = enabled
    }
    Row(
        modifier =
            modifier
                .clickable(
                    onClick = onClick,
                    enabled = enabled,
                    interactionSource = interactionSource,
                    indication = null,
                )
                .styleable(styleState, baseGradientButtonStyle then style),
        content = content,
    )
}

ตอนนี้คุณสามารถใช้สถานะ interactionSource เพื่อขับเคลื่อนการแก้ไขสไตล์ด้วยตัวเลือก 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 เพื่อเพิ่มภาพเคลื่อนไหวระหว่างสถานะต่างๆ โดยอัตโนมัติ ซึ่งคล้ายกับ API animate*AsState ตัวอย่างต่อไปนี้จะเปลี่ยน 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 การเปลี่ยนสีแบบเคลื่อนไหวเมื่อกด

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

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 แตกต่างกันไปตามสถานะการเล่นของเพลเยอร์ ทำตามขั้นตอนต่อไปนี้เพื่อสร้างและใช้สถานะที่กำหนดเอง

  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 ในคอมโพสได้และตั้งค่า 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)
}