الحالة والصور المتحركة في "الأنماط"

تقدّم Styles API طريقة إعلانية ومبسّطة لإدارة تغييرات واجهة المستخدم أثناء حالات التفاعل، مثل hovered وfocused وpressed. باستخدام واجهة برمجة التطبيقات هذه، يمكنك تقليل رمز النص النموذجي بشكل كبير، وهو الرمز المطلوب عادةً عند استخدام المعدِّلات.

لتسهيل تصميم العناصر التفاعلية، تعمل السمة StyleState كواجهة ثابتة للقراءة فقط تتتبّع الحالة النشطة لعنصر معيّن (مثل حالته المفعّلة أو المضغوطة أو التي تم التركيز عليها). ضمن StyleScope، يمكنك الوصول إلى هذه السمة من خلال السمة state لتنفيذ منطق شرطي مباشرةً في تعريفات النمط.

التفاعل المستند إلى الحالة: Hovered وfocused وpressed وselected وenabled وtoggled

تتضمّن الأنماط دعمًا مدمجًا للتفاعلات الشائعة:

  • مضغوط
  • تم التمرير على
  • تم الاختيار.
  • مفعّلة
  • تم التبديل

من الممكن أيضًا دعم الحالات المخصّصة. راجِع قسم تصميم الحالات المخصّصة باستخدام 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 لإضافة رسوم متحركة تلقائيًا بين الحالات المختلفة. يشبه ذلك واجهات برمجة التطبيقات animate*AsState. تحرّك المثال التالي borderColor من الأسود إلى الأزرق عندما تتغيّر الحالة إلى focused:

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 السمة 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)
}