For example, "Compose" is composed for the first time. Now, as Compose xiaobai, I am becoming more and more interested in composing for the first time.Copy the code
This article is a very simple page UI, which is some new controls + Navigation. I have seen some cases of Flutter implementation before, so I recently thought of using Compose to try it. The feel of Flutter is not very different from that of Flutter.
For example, for composing, you can view each other’s articles as an Activity, and for the rest, the Compose control is composed + N * Screen. This Screen is like a Fragment, but more flexible.
Compose can also handle the lifecycle via onActive, onPreCommit, onCommit, and onDispose, respectively:
> Compose function is rendered to the screen for the first time -> onActive > Compose function before each execution -> onPreCommit > Compose function each execution ->onCommit > screen re-render, remove from the screen ->onDisposeCopy the code
I. Login registration page:
The following controls are used:
1. Top TopAppBar, part of scaffolding
/** * TopBar * Scaffolds appBar is not limited to TopAppBar controls and may be any other arbitrary or customized@ComposeTopBar:@Composable () -> Unit = {},
* */
@Composable
fun TopBarView(
iconEvent: @Composable(() - >Unit)? = null,
titleText: String,
actionEvent: @Composable RowScope. () - >Unit= {}, {
TopAppBar(
title = {
Text(
text = titleText,
color = Color.Black
)
},
navigationIcon = iconEvent,
actions = actionEvent,
// below line is use to give background color
backgroundColor = Color.White,
contentColor = Color.Black,
elevation = 12.dp
)
}
/ / the source code:
@Composable
fun TopAppBar(
title: @Composable() - >Unit.// Title, not limited to text, can be customized
modifier: Modifier = Modifier, / / modifier, warpContent matchParent, size, background, pading, etc
navigationIcon: @Composable(() - >Unit)? = null.// Navigation button, can be any button, IconButton,TextButton...
actions: @Composable RowScope. () - >Unit = {}, // Right navigation, can be any key
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor).// Content color
elevation: Dp = AppBarDefaults.TopAppBarElevation // Projection height
)
Copy the code
- Switch the plain ciphertext mode of the password.
If the monitor gets the focus and the eye key is selected by the user, the input box is ciphertext:
/ / the source code
@Composable
fun TextField(
value: String,
onValueChange: (String) - >Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
label: @Composable(() - >Unit)? = null,
placeholder: @Composable(() - >Unit)? = null,
leadingIcon: @Composable(() - >Unit)? = null,
trailingIcon: @Composable(() - >Unit)? = null,
isError: Boolean = false.// Set this property after the input box gets the focus
/** * Interface used for changing visual output of the input field. * * This interface can be used for changing visual output of the text in the input field. * For example, you can mask characters in password filed with asterisk with * [PasswordVisualTransformation]. */
visualTransformation: VisualTransformation = VisualTransformation.None, // The default is plain text, that is, the user name
/ / /...
)
Copy the code
Modifier has a monitored by focal correction, once found gains focus here, can set the input box visualTransformation to PasswordVisualTransformation () :
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
// Only when the input box gets the focus and the input box type is password, can the focus be really captured. In this case, the mask face animation needs the top mask eyes animation to be executed
viewModel.onFocusHide(it.isFocused && (type == "password" || type == "rePassword"))},// Set the password input type
val visualTransformation =
// If the user clicks mask password and the input field is password type, the input field is ciphertext, otherwise it is plain text
if(! viewModel.showPwd && (type =="password" || type == "rePassword")) PasswordVisualTransformation() else VisualTransformation.None
Copy the code
ShowPwd and onFocusHide are variables defined by the mutableStateOf(Boolean) types themselves, which maintain the STATE of the UI.
2. Page simple routing, Navigation:
/ / navigation
implementation "Androidx. Navigation: navigation - compose: 2.4.0 - alpha09"
Copy the code
/** * define the route ** /
object PageRoute {
const val LOGIN_ROUTE = "login_route"
const val REGISTER_ROUTE = "register_route"
const val MAIN_ROUTE = "main_route"
}
/** * Associate pages with routes * First initialize a PageNavController instance: * PageNavController = rememberNavController() ** /
@ExperimentalPagerApi
@Composable
fun PageNavHost(mainActivity: MainActivity) {
val navHostController = MainActivity.pageNavController!!
val isLogined = false // Whether to log in
// Initial route destination
val initRoute = if (isLogined) PageRoute.MAIN_PAGE else PageRoute.LOGIN_ROUTE
NavHost(navController = navHostController, startDestination = initRoute) {
// Define routing, registration page, login page
composable(route = PageRoute.LOGIN_ROUTE) {
LoginPage(activity = mainActivity)
}
composable(route = PageRoute.REGISTER_ROUTE) {
RegisterPage(activity = mainActivity)
}
}
}
/** * redirects to ** /
fun doPageNavigationTo(route: String) {
val navController = MainActivity.pageNavController!!
navController.navigate(route) {
launchSingleTop = false
popUpTo(navController.graph.findStartDestination().id) {
// Prevent state loss
saveState = true
}
// Restore the composeble state
restoreState = true}}/** * page rollback ** /
fun doPageNavBack(route: String?). {
valnavController = MainActivity.pageNavController!! route? .let { navController.popBackStack(route = it, inclusive =false)}? : navController.popBackStack() }Copy the code
Second, home page structure:
-
The bottom navigation is defined at the bottom of the mainScreen and corresponds to each of the four Compose Screen pages, with the first Screen selected by default (homeScreen).
-
HomeScreen should have a search box at the top and a sliding list of columns, with the first column selected by default.
1. Bottom navigation bar:
- MainScreen:
The Scaffold provides the bottom navigation buttomBar, which provides the BottomNavigationScreen with the navigation controller and the bottom element dataList.
/** ** ** */
fun MainPage(a) {
// The bottom navigation corresponds to the page
val list = listOf(
Screens.Home,
Screens.Ranking,
Screens.Favorite,
Screens.Profile,
)
val navController = rememberNavController()
Scaffold(bottomBar = {
// The bottom navigation, in fact, Compose is very flexible to the bottomBar, like the appBar, can pass in any @compose
BottomNavigationScreen(navController = navController, items = list)
}) {
// The bottom navigation route defines the navigation linkage
BottomNavHost(navHostController = navController)
}
}
Copy the code
-
Bottom navigation bar
/** * Bottom navigation bar ** / @Composable fun BottomNavigationScreen(navController: NavController, items: List<Screens>) { val navBackStackEntry by navController.currentBackStackEntryAsState() valdestination = navBackStackEntry? .destination BottomNavigation(backgroundColor = Color.White,elevation =12.dp) { items.forEach { screen -> // Each option at the bottomBottomNavigationItem( selected = destination? .route == screen.route,// Click response jump onClick = { navController.navigate(screen.route) { launchSingleTop = true popUpTo(navController.graph.findStartDestination().id) { // Prevent state loss saveState = true } // Restore the Composable state restoreState = true } }, icon = { Icon( painter = painterResource(id = screen.icons), contentDescription = null ) }, label = { Text(screen.title) }, alwaysShowLabel = true, unselectedContentColor = gray400, selectedContentColor = bili_90, ) } } } Copy the code
-
Define the bottom navigation information, define the text, icon and routing address:
/** ** */ sealed class Screens(val title: String, val route: String, @DrawableRes val icons: Int) { object Home : Screens(title = "Home page", route = "home_route", icons = R.drawable.round_home_24) object Ranking : Screens(title = "Top", route = "ranking_route", icons = R.drawable.round_filter_24) object Favorite : Screens(title = "Collection", route = "fav_route", icons = R.drawable.round_favorite_24) object Profile : Screens(title = "I", route = "profile_route", icons = R.drawable.round_person_24) } Copy the code
-
Define the route of the page corresponding to the bottom navigation bar
This is the same as the login route, but the routing address here is defined under the sealed Screens class. This is also recommended, for the convenience of managing the linkage between the options at the bottom and the page.
/** * set Home as the default page ** / @Composable fun BottomNavHost(navHostController: NavHostController) { NavHost(navController = navHostController, startDestination = Screens.Home.route) { composable(route = Screens.Home.route) { HomeTabPage() } composable(route = Screens.Ranking.route) { RankingPage() } composable(route = Screens.Favorite.route) { FavoritePage() } composable(route = Screens.Profile.route) { ProfilePage() } } } Copy the code
2. Top slide list:
-
The main thing is to dynamically scroll the ViewPager based on the slide list, and in turn the ViewPage scroll is linked to the slide list.
-
Compose provides the same component as ViewPager, the Pager, divided into HorizontalPager and VerticalPager.
About Pager can see Google’s documentation, recommend a better use of Pager to achieve banner rotation case, from the big guy Zhu JiangCompose Banner
/ / similar to the ViewPager
implementation "com.google.accompanist:accompanist-pager:$accompanist_version"
Copy the code
- Sliding tabView component, at androidx.com pose. Material provided under the TAB in two styles, the slide and slide:
-
TabView: unscrollable, child elements of equal width, text direction changes from horizontal to vertical when the arrangement is too large. Suitable for the following scenarios:
-
ScrollableTabRow can be scrolled, depending on the contents of the package.
/ / > source
@Composable
fun TabRow(
selectedTabIndex: Int.// The selected subscript
modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor).// The indicator is full by default. The default indicator is used here
indicator: @Composable (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
TabRowDefaults.Indicator(
Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
)
},
divider: @Composable() - >Unit = @Composable {
TabRowDefaults.Divider()
},
// The contents are Tab arrays
tabs: @Composable() - >Unit
)
// The ScrollableTabRow argument is the same as the TabRow argument, but with two more arguments:
// first spacing pading
edgePadding: Dp = TabRowDefaults.ScrollableTabRowPadding,
// The line separating the bottom from the adjacent content
divider: @Composable() - >Unit = @Composable {
TabRowDefaults.Divider()
},
Copy the code
- Slider TabView:
val items = listOf("Recommended"."Film"."TV drama"."Variety"."Documentary"."Entertainment"."News")
val tabstate = remember {
mutableStateOf(items[0])}val pagerState = rememberPagerState(
//pageCount = items. Size, // Total pages
//initialOffscreenLimit = 3, // The number of preloads
//infiniteLoop = false, // whether to loop indefinitely
initialPage = 0 // Initial page
)
// Slide TabView
ScrollableTabRow(
selectedTabIndex = items.indexOf(tabstr.value),
modifier = Modifier.wrapContentWidth(),
edgePadding = 16.dp,
// Use the default indicator, which can be customized
indicator = { tabIndicator ->
TabRowDefaults.Indicator(
Modifier.tabIndicatorOffset(
tabIndicator[items.indexOf(
tabstate.value
)]
),
color = Color.Cyan
)
},
// The background color, with a TAB column, will block the background, with the length of the edgePadding at the beginning and end
backgroundColor = colorResource(id = R.color.purple_500),
// The bottom separator line can be used by default to isolate the following text
divider = {
TabRowDefaults.Divider(color = Color.Gray)
}
) {
items.forEachIndexed { index, title ->
val selected = index == items.indexOf(tabstr.value)
Tab(
modifier = Modifier.background(color = colorResource(id = R.color.purple_200)),
text = { Text(title, color = Color.White) },
selected = selected,
selectedContentColor = colorResource(id = R.color.purple_500),
onClick = {
// Here tabView is associated with pager
tabstate.value = items[index]
scope.launch {
// Pager toggle
pagerState.scrollToPage(index)
}
}
)
}
}
// Horizontal Pager is similar to PagerView
HorizontalPager(
state = pageState,
count = items.size,
reverseLayout = false
) { indexPage ->
// Here is Pager's content
Column(
Modifier
.fillMaxSize()
.background(Color.White),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
when (indexPage) {
in 0..(items.size) -> Text(text = items[indexPage])
}
}
}
Copy the code
3. TabView indicator
*@param Indicator Indicates which TAB is currently selected. By default, is a [TabRowDefaults Indicator], use [TabRowDefaults. TabIndicatorOffset]
* modifier to determine its position. Note that this indicator will be forced to fill the entire TabRow, so you should use [TabRowDefaults. TabIndicatorOffset] or similar to modify the offset
As you can see, tabIndicatorOffset is mainly used to animate indicators
/ / > source
fun Modifier.tabIndicatorOffset(
currentTabPosition: TabPosition
): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "tabIndicatorOffset"
value = currentTabPosition
}
) {
val currentTabWidth by animateDpAsState(
targetValue = currentTabPosition.width,
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing)
)
// animateDpAsState is an animation wrapper that returns a variable value as the animation progresses, somewhat like a valuer
val indicatorOffset by animateDpAsState(
targetValue = currentTabPosition.left,
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing)
)
fillMaxWidth()
.wrapContentSize(Alignment.BottomStart)
.offset(x = indicatorOffset) // The offset of the animation move
.width(currentTabWidth)
}
Copy the code
- Custom indicators:
Custom designators can be added to the designators with desired animations, styles, and colors. Here I want to define an indicator that slides with tabs.
Requirements:
- You can set height width with rounded corners.
- The indicator has a shrinkage effect, similar to a worm wriggling effect, which makes the extension direction extend faster and the contraction direction slow.
// 1. How to draw the indicator? Use draw to draw a line or image, or use the Modifier to set a Backaground, where Shape uses a rounded rectangle
@Composable
fun BiliIndicator(
height: Dp = TabRowDefaults.IndicatorHeight,
color: Color = bili_50,
modifier: Modifier = Modifier
) {
Box(
modifier
// Restrict the size and position of each indicator's children so as not to fill the entire Tab
.padding(top = 5.dp,bottom = 2.dp, start = 16.dp, end = 16.dp)
.fillMaxWidth()
.height(height)
// If you do not use background, use border that is a box
.background(color = color,shape = RoundedCornerShape(size = 4.dp))
// Same effect as above, want to draw also can draw:
/*.drawWithContent(onDraw = { drawLine( color = color, strokeWidth = 8f, start = Offset(0f, size.height), end = Offset(size.width, size.height), pathEffect = PathEffect.cornerPathEffect(radius = 5f) ) })*/)}// 2. The animation definition of the indicator. The style of the indicator is drawn when the animation is executed
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun BiliAnimatedIndicator(tabPositions: List<TabPosition>, selectedTabIndex: Int) {
// You can specify some color, one color for each slider, or you can use the default
val colors = listOf(Color.Yellow, Color.Red, Color.Green)
// Transition is used to process animation and manage animation. Target is the selected TAB
val transition = updateTransition(selectedTabIndex, label = "Transition")
// Define the starting point and animate the distance
val indicatorStart by transition.animateDp(
label = "Indicator Start",
transitionSpec = {
// If you move to the right, the right moves faster
// If you move to the left, the left side is faster
// Spring provides an elastic space, a kind of AnimatorSpace, similar to the concept of tween and keyframes.
// dampingRatio defaults to 1f,
// The opposite of the stiffness is the flexibility. The greater the stiffness, the faster the spring expands. The lower value is set here, because the starting point is slower when sliding to the right
// Compare the subscript, target > start, indicating a right slide
if (initialState < targetState) {
spring(dampingRatio = 1f, stiffness = 50f)}else {
// The starting point of the left slide is fast and the stiffness is large
spring(dampingRatio = 1f, stiffness = 1000f)}}) {// TabPosition has left,right, and width attributes, which describe the position and size of a TAB in the entire tabView
// left + width = right
tabPositions[it].left
}
// Define the endpoint
val indicatorEnd by transition.animateDp(
label = "Indicator End",
transitionSpec = {
if (initialState < targetState) {
spring(dampingRatio = 1f, stiffness = 1000f)}else {
spring(dampingRatio = 1f, stiffness = 50f)
}
}
) {
tabPositions[it].right
}
// An optional list of colors specified by colors
val indicatorColor1 by transition.animateColor(label = "Indicator Color") {
colors[it % colors.size]
}
//val indicatorColor2 = ColorUtil.getRandomColor(bili_50)
// Draws the indicator subitems defined earlier
BiliIndicator2(
// indicator Current color, specify the color has a default value
//color = indicatorColor1,
height = 5.dp,
modifier = Modifier
// Fill the entire TabView and place the indicator in the starting position
.fillMaxSize()
.wrapContentSize(align = Alignment.BottomStart)
// Set the offset to the starting position of the indicator
.offset(x = indicatorStart)
// As you move between tabs, the indicator width matches the animation width
.width(indicatorEnd - indicatorStart)
)
}
Copy the code
After drawing the indicator, replace indicator = XXX in the ScrollTabView above and you will see the following: