Առաջարկեք հարմարեցված 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.