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

توفّر 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 = 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 من الأسود إلى الأزرق عند تغيير الحالة إلى "محدّد":

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، ولامدا مع الحالة المحدّدة، والنمط.

// 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(block: () -> Unit) {
    state(playerStateKey, block, { key, state -> state[key] == PlayerState.Playing })
}
fun StyleScope.playerPaused(block: () -> Unit) {
    state(playerStateKey, block, { 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(block: () -> Unit) {
    state(playerStateKey, block, { key, state -> state[key] == PlayerState.Playing })
}
fun StyleScope.playerPaused(block: () -> Unit) {
    state(playerStateKey, block, { 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)
}