Առաջարկեք հարմարեցված Android թեմաներ ձեր օգտատերերին

Jetpack Compose-ը թարմացնում է Android-ում թեմաները մշակելու հին ձևը: Այն առաջարկում է մեծ ճկունություն, ինչը մեզ ավելի շատ հնարավորություններ է տալիս մեր միջերեսի արտաքին տեսքի մեր սահմանման հարցում: Միևնույն ժամանակ, կոմպոզիցիոն անիմացիոն համակարգը մեզ հնարավորություն է տալիս հեշտությամբ ստեղծել ավելի հավակնոտ և հաճելի UI անիմացիաներ: Այս ձեռնարկում ես կհամատեղեմ այս երկուսը թեմաների միջև անցման անիմացիա ստեղծելու համար: Վերջնական արդյունքը կունենա հետևյալ տեսքը.

Մենք կօգտագործենք AnimatedContent-ը այս անիմացիայի հասնելու համար: Սա բաղադրելի է, որը ցանկացած օբյեկտ ընդունում է որպես վիճակ և բովանդակություն՝ ցուցադրելու համար: Ամեն անգամ, երբ այդ վիճակը փոխվում է, այն աշխուժացնում է նախորդ բովանդակությունից դեպի նոր բովանդակություն՝ օգտագործելով նոր վիճակը:

Նախ, մենք պետք է սահմանենք օբյեկտ, որը պետք է անցնի որպես վիճակ, որը կպարունակի մեր ընթացիկ թեմայի տվյալները:

data class CustomTheme(  
    val primaryColor: Color,  
    val background: Color,  
    val textColor: Color,  
    val image: Int,  
)  
  
val darkTheme = CustomTheme(  
    primaryColor = Color(0xFFE9B518),  
    background = Color(0xFF111111),  
    textColor = Color(0xffFFFFFF),  
    image = R.drawable.dark,  
)  
  
val lightTheme = CustomTheme(  
    primaryColor = Color(0xFF2CB6DA),  
    background = Color(0xFFF1F1F1),  
    textColor = Color(0xff000000),  
    image = R.drawable.light,  
)  
  
val pinkTheme = CustomTheme(  
    primaryColor = Color(0xFFF01EE5),  
    background = Color(0xFF110910),  
    textColor = Color(0xFFEE8CE1),  
    image = R.drawable.pink,  
)

Այստեղ ես սահմանել եմ տվյալների դաս և երեք թեմա՝ յուրահատուկ գույներով:

Այժմ մենք կարող ենք իրականացնել AnimatedContent-ը և օգտագործել այս օբյեկտը որպես վիճակ:

@ExperimentalAnimationApi  
@Composable  
fun App() {  
    var theme by remember { mutableStateOf(lightTheme) }  
    AnimatedContent(  
        targetState = theme,  
        modifier = Modifier  
            .background(Color.Black)  
            .fillMaxSize(),  
    ) { currentTheme ->  
        Surface(  
            modifier = Modifier  
                 .fillMaxSize(),  
            color = currentTheme.background  
 ) {  
            Box {  
                Box(  
                    modifier = Modifier  
                        .fillMaxWidth()  
                        .height(300.dp)  
                ) {  
                    Image(  
                        painter = painterResource(id = currentTheme.image),  
                        contentDescription = "headerImage",  
                        contentScale = ContentScale.Crop,  
                    )  
                    Box(  
                        modifier = Modifier  
                            .fillMaxSize()  
                            .background(  
                                brush = Brush.verticalGradient(  
                                    colors = listOf(  
                                        Color.Transparent,  currentTheme.background.copy(alpha = .2f),  
                                        currentTheme.background  
                                    )  
                                )  
                            )  
                    )  
                }  
  
                Row(  
                    modifier = Modifier  
                        .align(Alignment.Center),  
                    horizontalArrangement = Arrangement.Center,  
                    verticalAlignment = Alignment.CenterVertically,  
                ) {  
  
                    ThemeButton(  
                        theme = lightTheme,  
                        currentTheme = currentTheme,  
                        text = "Light",  
                    ) {  
                        theme = lightTheme  
                    }  
  
                    ThemeButton(  
                        theme = darkTheme,  
                        currentTheme = currentTheme,  
                        text = "Dark",  
                    ) {  
                        theme = darkTheme  
                    }  
  
                    ThemeButton(  
                        theme = pinkTheme,  
                        currentTheme = currentTheme,  
                        text = "Pink",  
                    ) {  
                        theme = pinkTheme  
                    }  
                }  
            }  
        }  
    }  
}

theme վիճակը սկզբնավորվել է և անցել AnimatedContentին: Բովանդակության ներսում currentTheme-ը փոխանցվում է մեր միջերեսը թեմատիկացնելու համար:

Նկատի ունեցեք, որ մենք պետք է օգտագործենք սա և ոչ թե themeը, որպեսզի նախկին բովանդակությունն անմիջապես չանցնի նոր թեմային, երբ վիճակը փոխվի:

Գոյություն ունի պարզ միջերես, որը սահմանվում է վերնագրի պատկերով և երեք կոճակով՝ հասանելի թեմաների միջև անցնելու համար: Այս պահին մենք կունենանք այսպիսի անիմացիա.

Սա լռելյայն անիմացիան է, որը գալիս է AnimatedContent-ով:

Դա լավ է, բայց մենք պետք է փոխենք սա, որպեսզի հասնենք շրջանագծի բացահայտման անիմացիան վերջնական անիմացիայի մեջ:

transitionSpec = {  
    fadeIn(  
        initialAlpha = 0f,  
        animationSpec = tween(100)  
    ) with fadeOut(  
        targetAlpha = .9f,  
        animationSpec = tween(800)  
    ) + scaleOut(  
        targetScale = .95f,  
        animationSpec = tween(800)  
    )  
}

Սա սովորական անիմացիա է, որը մենք պետք է անցնենք AnimatedContent-ին: Նոր բովանդակությունը գրեթե ակնթարթորեն կթուլանա, մինչդեռ հին բովանդակությունը ավելի երկար ժամանակի ընթացքում ունի նուրբ մարում և մասշտաբ: Նոր բովանդակությունը արագ մարում է, որպեսզի մենք կարողանանք անմիջապես սկսել բացահայտման անիմացիան: AnimatedContent-ում վիճակները փոխելիս նոր բովանդակությունը նոր բաղադրելի է, ուստի այն գործարկում է իր LaunchedEffect-ը: Մենք կսկսենք անիմացիան այստեղից և կօգտագործենք արժեքը՝ նոր բովանդակության վրա շրջանաձև տեսահոլովակ անիմացիայի համար:

...
var theme by remember { mutableStateOf(pinkTheme) }  
var animationOffset by remember { mutableStateOf(Offset(0f, 0f)) }
AnimatedContent(  
    ...
) { currentTheme ->
val revealSize = remember { Animatable(1f) }  
    LaunchedEffect(key1 = "reveal", block = {  
        if (animationOffset.x > 0f) {  
            revealSize.snapTo(0f)  
            revealSize.animateTo(1f, animationSpec = tween(800))  
        } else {  
            revealSize.snapTo(1f)  
        }  
    })  
 
    Box(  
        modifier = Modifier  
   .fillMaxSize()  
            .clip(CirclePath(revealSize.value, animationOffset))  
    ) {  
        Surface(
...

animationOffset վիճակը սահմանում է շրջանի անիմացիայի սկզբնակետը: Սա ավելի ուշ կսահմանվի ThemeButton-ի ներսում: revealSize աշխուժացնում է այն շրջանակը, որը կտրում է նոր բովանդակությունը:

LaunchedEffect-ին մենք սկսում ենք շրջանակի տեսահոլովակի անիմացիան, եթե ունենք վավեր սկզբնակետ: Եթե ​​ոչ, դա նշանակում է, որ սա առաջին վերակազմավորումն է, երբ մենք պարզապես բացում ենք այս էկրանը, այնպես որ մենք պարզապես կտրում ենք անիմացիան մինչև վերջ:

Հաջորդը, մենք Surface-ը փաթաթում ենք մի տուփով, որը սեղմում է այն:

Ուշադրություն դարձրեք, որ օգտագործված ձևը հարմարեցված է: Սրա պատճառն այն էր, որ լռելյայն CircleShape-ը պարզապես կլորացված ուղղանկյուն է՝ բարձր շառավղով, և ես դրանով չկարողացա հասնել ցանկալի տեսքի:

class CirclePath(private val progress: Float, private val origin: Offset = Offset(0f, 0f)) : Shape {  
    override fun createOutline(  
        size: Size,  
        layoutDirection: LayoutDirection,  
        density: Density  
 ): Outline {  
  
        val center = Offset(  
            x = size.center.x - ((size.center.x - origin.x) * (1f - progress)),  
            y = size.center.y - ((size.center.y - origin.y) * (1f - progress)),  
        )  
        val radius = (sqrt(  
            size.height * size.height + size.width * size.width  
        ) * .5f) * progress  
  
  return Outline.Generic(  
            Path().apply {  
                addOval(  
                    Rect(  
                        center = center,  
                        radius = radius,  
                    )  
                )  
            }  
        )  
    }  
}

CirclePath ձևը վերցնում է լողալ, որը սահմանում է մինչ այժմ առաջընթացը և անիմացիայի սկզբնակետը:

Սրանցից երկուսն էլ, և չափը, օգտագործվում են շրջանակի բացահայտման անիմացիա ստեղծելու համար, որն ընդգրկում է ամբողջ բովանդակությունը:

Վերջին բանը, որ պետք է անել, կլինի սահմանել անիմացիայի ծագումը, երբ կոճակը սեղմվի: Այս արժեքը գտնվում է ThemeButton-ում և փոխանցվում է կոճակի սեղմման ժամանակ:

@Composable  
fun ThemeButton(  
    theme: CustomTheme,  
    currentTheme: CustomTheme,  
    text: String,  
    onClick: (Offset) -> Unit,  
) {  
    val isSelected = theme == currentTheme  
 var offset: Offset = remember { Offset(0f, 0f) }  
    Column(  
        horizontalAlignment = Alignment.CenterHorizontally  
 ) {  
        Box(  
            modifier = Modifier  
    .onGloballyPositioned {  
                    offset = Offset(  
                        x = it.positionInWindow().x + it.size.width / 2,  
                        y = it.positionInWindow().y + it.size.height / 2  
     )  
                }  
                .size(110.dp)  
                .border(  
                    4.dp,  
                    color = if (isSelected) theme.primaryColor else Color.Transparent,  
                    shape = CircleShape  
    )  
                .padding(8.dp)  
                .background(color = theme.primaryColor, shape = CircleShape)  
                .clip(CircleShape)  
                .clickable {  
                    onClick(offset)  
                }  
        ) {  
            Image(  
                modifier = Modifier.fillMaxSize(),  
                painter = painterResource(id = theme.image),  
                contentDescription = "themeImage",  
                contentScale = ContentScale.Crop,  
            )  
        }  
  
        Text(  
            text = text.uppercase(),  
            modifier = Modifier  
                .alpha(if (isSelected) 1f else .5f)  
                .padding(2.dp),  
            color = currentTheme.textColor,  
            fontWeight = FontWeight.Bold,  
            fontSize = 20.sp  
        )  
    }  
}

Ահա ThemButton-ի սահմանումը. Ինչպես տեսնում եք, սեղմելիս ուղարկվում է կոճակի կենտրոնական օֆսեթը:

Այնուհետև մենք կարող ենք սա սահմանել որպես շրջանակի բացահայտման անիմացիայի սկզբնակետ, այսպես.

ThemeButton(  
    ...  
) {  
    animationOffset = it  
    theme = lightTheme  
}  
  
ThemeButton(  
    ...
) {  
    animationOffset = it  
    theme = darkTheme  
}  
  
ThemeButton(  
    ... 
) {  
    animationOffset = it  
    theme = pinkTheme  
}

Եվ վերջ: Այժմ մենք ունենք հատուկ թեմա ընտրող անիմացիա, որն անպայման կուրախացնի մեր օգտատերերին: Ամբողջական աղբյուրի կոդը հասանելի է այստեղ:

Շնորհակալություն կարդալու համար և հաջողություն:

Want to Connect?
Originally published at https://sinasamaki.com.