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

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

لتسهيل التنسيق التفاعلي، تعمل 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. حالة التمرير والضغط معًا على زر

عناصر قابلة للإنشاء مخصّصة باستخدام Modifier.styleable

عند إنشاء مكونات styleable خاصة بك، يجب ربط interactionSource بـ styleState. بعد ذلك، مرِّر هذه الحالة إلى Modifier.styleable للاستفادة منها.

لنفترض أنّ نظام التصميم يتضمّن GradientButton. قد تحتاج إلى إنشاء LoginButton يرث من GradientButton، ولكن يغيّر ألوانه أثناء التفاعلات، مثل الضغط عليه.

  • لتفعيل تحديثات نمط interactionSource، أدرِج interactionSource كمَعلمة ضمن العنصر القابل للإنشاء. استخدِم المَعلمة المقدَّمة، أو إذا لم يتم توفيرها، ابدأ MutableInteractionSource جديدًا.
  • ابدأ عملية styleState من خلال تقديم interactionSource. تأكَّد من أنّ حالة التفعيل في 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 لتعديل الأنماط باستخدام الخيارات 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 من الأسود إلى الأزرق عند تغيير الحالة إلى "في وضع التركيز":

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 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 القابل للإنشاء استنادًا إلى حالة التشغيل في المشغّل. اتّبِع الخطوات التالية لإنشاء حالتك المخصّصة واستخدامها:

  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 في المعدِّل.

ضمن دالة style lambda، يمكنك تطبيق نمط مستند إلى الحالة للحالات المخصّصة، باستخدام دوال الإضافة التي تم تحديدها سابقًا.

في ما يلي المقتطف الكامل لهذا المثال:

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