حالت‌ها و انیمیشن‌ها در استایل‌ها

API استایل‌ها یک رویکرد اعلانی و ساده برای مدیریت تغییرات رابط کاربری در طول حالت‌های تعاملی مانند hovered ، focused و pressed ارائه می‌دهد. با استفاده از این API، می‌توانید کد تکراری که معمولاً هنگام استفاده از اصلاح‌کننده‌ها مورد نیاز است را به میزان قابل توجهی کاهش دهید.

برای تسهیل استایل‌دهی واکنشی، StyleState به عنوان یک رابط پایدار و فقط خواندنی عمل می‌کند که وضعیت فعال یک عنصر (مانند وضعیت فعال، فشرده یا متمرکز) را ردیابی می‌کند. در یک StyleScope ، می‌توانید از طریق ویژگی state به این دسترسی داشته باشید تا منطق شرطی را مستقیماً در تعاریف Style خود پیاده‌سازی کنید.

تعامل مبتنی بر وضعیت: معلق ماندن روی ماوس، فوکوس، فشردن، انتخاب شدن، فعال شدن، تغییر وضعیت

استایل‌ها با پشتیبانی داخلی برای تعاملات رایج ارائه می‌شوند:

  • فشرده شده
  • معلق
  • انتخاب شده
  • فعال شده
  • فعال/غیرفعال

همچنین می‌توان از حالت‌های سفارشی پشتیبانی کرد. برای اطلاعات بیشتر به بخش «طراحی حالت سفارشی با StyleState» مراجعه کنید.

مدیریت حالت‌های تعامل با پارامترهای Style

مثال زیر تغییر 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)
            })
        }
    )
}

شکل ۱. تغییر رنگ پس‌زمینه بر اساس حالت‌های موس و فوکوس

شما همچنین می‌توانید تعاریف حالت تو در تو ایجاد کنید. برای مثال، می‌توانید یک سبک خاص برای زمانی که یک دکمه همزمان فشرده و نگه داشته می‌شود، تعریف کنید:

@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)
            })
        }
    )
}

شکل ۲. حالت‌های نگه‌داشتن موس و فشردن همزمان روی یک دکمه.

ترکیب‌های سفارشی با Modifier.styleable

هنگام ایجاد کامپوننت‌های styleable خودتان، باید یک interactionSource به یک styleState متصل کنید. سپس، این state را به Modifier.styleable ارسال کنید تا از آن استفاده شود.

سناریویی را در نظر بگیرید که سیستم طراحی شما شامل یک GradientButton باشد. ممکن است بخواهید یک LoginButton ایجاد کنید که از GradientButton ارث‌بری کند، اما رنگ‌های آن در طول تعاملات، مانند فشرده شدن، تغییر کند.

  • برای فعال کردن به‌روزرسانی‌های سبک interactionSource ، یک interactionSource به عنوان پارامتر در composable خود وارد کنید. از پارامتر ارائه شده استفاده کنید یا اگر پارامتری ارائه نشده است، یک MutableInteractionSource جدید را مقداردهی اولیه کنید.
  • با ارائه interactionSource styleState مقداردهی اولیه کنید. مطمئن شوید که وضعیت enabled در styleState ، مقدار پارامتر enabled ارائه شده را نشان می‌دهد.
  • 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 برای اعمال تغییرات استایل با گزینه‌های فشرده‌شده، متمرکز و معلق‌شده در داخل بلوک استایل استفاده کنید:

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

شکل ۳. تغییر یک حالت قابل ترکیب سفارشی بر اساس interactionSource .

تغییرات سبک متحرک

تغییرات حالت استایل‌ها با پشتیبانی از انیمیشن داخلی ارائه می‌شوند. می‌توانید ویژگی جدید را درون هر بلوک تغییر حالت با animate قرار دهید تا به طور خودکار انیمیشن‌ها بین حالت‌های مختلف اضافه شوند. این مشابه APIهای animate*AsState است. مثال زیر وقتی حالت به focus تغییر می‌کند، 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)) {

    }
}

شکل ۴. متحرک‌سازی تغییرات رنگ هنگام فشار دادن.

API animate یک 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))
}

شکل ۵. متحرک‌سازی تغییرات اندازه و رنگ هنگام فشار دادن.

استایل‌بندی سفارشی حالت با StyleState

بسته به مورد استفاده‌ی Composable شما، ممکن است سبک‌های مختلفی داشته باشید که توسط حالت‌های سفارشی پشتیبانی می‌شوند. برای مثال، اگر یک برنامه‌ی رسانه‌ای دارید، ممکن است بخواهید بسته به حالت پخش پخش‌کننده، سبک‌بندی متفاوتی برای دکمه‌های موجود در MediaPlayer Composable خود داشته باشید. برای ایجاد و استفاده از حالت سفارشی خود، این مراحل را دنبال کنید:

  1. تعریف کلید سفارشی
  2. ایجاد افزونه StyleState
  3. پیوند به حالت سفارشی

تعریف کلید سفارشی

برای ایجاد یک استایل سفارشی مبتنی بر وضعیت، ابتدا یک StyleStateKey ایجاد کنید و مقدار پیش‌فرض وضعیت را به آن بدهید. وقتی برنامه اجرا می‌شود، پخش‌کننده رسانه در وضعیت Stopped قرار دارد، بنابراین به این روش مقداردهی اولیه می‌شود:

enum class PlayerState {
    Stopped,
    Playing,
    Paused
}

val playerStateKey = StyleStateKey(PlayerState.Stopped)

ایجاد توابع افزونه StyleState

یک تابع افزونه روی StyleState تعریف کنید تا playState فعلی را جستجو کند. سپس، توابع افزونه‌ای روی StyleScope ایجاد کنید که حالت‌های سفارشی شما را در playStateKey ، یک لامبدا با حالت خاص و سبک مورد نظر ارسال می‌کنند.

// 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 روی اصلاح‌کننده ارسال کنید.

درون 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)
}