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:
  1. 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.

  2. 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
  1. 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
  1. 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:

  1. You can set height width with rounded corners.
  2. 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: