I’ve been coming into contact with Compose for a while now. As a practice, I wrote a small application similar to Bilibili. The module includes:

  • Home page list
  • Play details, comment like
  • Start page
  • Log in to register
  • Nested sliding layout
  • Sweep code identification

It took a while, but it’s not perfect but it’s ready to share. The deficiencies will be updated later.

In the previous article mainly records the login registration page and the realization of the routing framework left Jetpack Compose | low imitation bi li bi li (a) | login attempt to achieve registration | home page frame

Effect:

One, the implementation of the start page:

  1. First, you define the page route, defining the initial route startDestination as the start page SplashScreen.

  2. Then add animation and other dynamic effects to the startup page, during which you can do other things, such as request advertisement picture, authentication to determine whether token exists, login expiration, if need to log in again, Navigation to LoginScreen, otherwise jump to MainScreen.

  3. There’s nothing special about my launch page here, just some action effects.

  • Check whether the local login password exists. If it does not exist, you need to log in again:
class SplashViewModel : ViewModel() {

    var isEndTask by mutableStateOf(false)
    var checkingLoginState = mutableStateOf(false)
    var loginState by mutableStateOf(-1)

    // Check login
    private fun checkLogin(a) {
        checkingLoginState.value = true
        viewModelScope.launch {

            // Query whether the login token has been saved to simulate whether you have logged in
            flow {
                delay(5000) // There is a delay just for animation execution
                val boardingPass = DataStoreUtil.readStringData(BOARDING_PASS)
                val result = if (boardingPass.isBlank()) 0 else 1
                emit(result)
            }.flowOn(Dispatchers.IO).collect {
                Log.d("splash--:"."loginState:$it")
                loginState = it
                checkingLoginState.value = false}}}fun checkLoginState(a) {
        viewModelScope.launch {
            delay(1000)
            checkLogin()
            isEndTask = true}}}Copy the code
  • The launch page has no special features other than animations
/** * Start page entry *@paramNavNexEvent Next action callback method, jump to login or home page */
@ExperimentalAnimationApi
@Composable
fun SplashScreen(
    splashViewModel: SplashViewModel,
    navNexEvent: (Boolean) - >Unit
) {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .navigationBarsPadding()
            .background(MaterialTheme.colors.background),
    ) {
        splashViewModel.checkLoginState()

        // Large background image
        SplashBgOne()

        Column(
            modifier = Modifier
                .animateContentSize()
                .wrapContentSize()
                .align(Alignment.BottomCenter),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Row(
                modifier = Modifier
                    .wrapContentHeight(),
                verticalAlignment = Alignment.CenterVertically
            ) {
                // coil
                CoilImage(
                    modifier = Modifier
                        .width(50.dp)
                        .height(115.dp),
                    url = R.drawable.bili_man
                )
                Column(
                    modifier = Modifier.background(
                        // Here is a linear gradient background
                        brush = Brush.verticalGradient(
                            colorStops = arrayOf(
                                0.0 f to Color.White,
                                0.2 f to bili_20,
                                0.5 f to Color.Transparent,
                                0.7 f to Color.White,
                                0.9 f to Color.Transparent
                            )
                        )
                    )
                ) {
                    Text(
                        modifier = Modifier.padding(start = 5.dp),
                        text = "Bili...",
                        fontSize = 35.sp,
                        fontWeight = FontWeight.Bold,
                        color = MaterialTheme.colors.onBackground
                    )
                    Text(
                        modifier = Modifier.padding(start = 5.dp, bottom = 6.dp),
                        text = "https://www.bilibili.com",
                        fontSize = 20.sp,
                        color = Color.Gray.copy(alpha = 0.7 f),
                        fontStyle = FontStyle.Italic
                    )
                }
            }
            Spacer(modifier = Modifier.height(28.dp))

            // The content wrapped by AnimatedVisibility will be displayed/hidden according to the visible condition set
            AnimatedVisibility(
                visible = splashViewModel.checkingLoginState.value,
                modifier = Modifier
                    .padding(top = 20.dp)

            ) {
                Column(horizontalAlignment = Alignment.CenterHorizontally) {
                    LinearProgressIndicator(
                        modifier = Modifier.width(150.dp)
                    )
                    Spacer(modifier = Modifier.height(20.dp))
                    Text(text = "Check login status...")
                    Spacer(modifier = Modifier.height(20.dp))
                }
            }
        }
    }
    LaunchedEffect launches a coroutine scope
    LaunchedEffect(
        splashViewModel.loginState,
        splashViewModel.checkingLoginState
    ) {
        if(splashViewModel.loginState ! = -1 && !splashViewModel.checkingLoginState.value) {
            // Proceed to the next step
            navNexEvent(splashViewModel.loginState == 1)}}}@ExperimentalAnimationApi
@Composable
fun SplashBgOne(a) {
    // Create three animation Spaces
    val alphaAnim = remember {
        Animatable(0f)}val cornerAnim = remember {
        Animatable(50f)}val scaleAnim = remember {
        Animatable(0.2 f)
    }

    
    LaunchedEffect(key1 = true) {
        // Add fade animation to the background
        alphaAnim.animateTo(targetValue = 1.0 f,              
            animationSpec = tween(
                durationMillis = 500,
                easing = {
                    OvershootInterpolator(0.8 f).getInterpolation(it)
                }
            )
        )
        // Add rounded corners to the background
        cornerAnim.animateTo(targetValue = 1.0 f,
            animationSpec = tween(
                durationMillis = 500,
                easing = {
                    OvershootInterpolator(0.8 f).getInterpolation(it)
                }
            )
        )
        // Add a zoom animation to the background
        scaleAnim.animateTo(targetValue = 1.0 f,
            animationSpec = tween(
                durationMillis = 300,
                easing = {
                    OvershootInterpolator(0.6 f).getInterpolation(it)
                }
            )
        )
    }
    // Add attributes to the image from the corresponding animation value
    Image(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxSize()
            .scale(scaleAnim.value)
            .alpha(alpha = alphaAnim.value)
            .clip(shape = RoundedCornerShape(1.dp.times(cornerAnim.value.toInt()))),
        //.align(Alignment.Center),
        painter = painterResource(id = R.drawable.splash_pic),
        contentScale = ContentScale.FillBounds,
        contentDescription = null)}Copy the code

Ii. Sliding effect of personal center interface

Page slide nesting, the official provides a Modifier called nestedScroll() to handle the slide, through which you can monitor the progress and direction of the slide. Google had plans to improve Compose’s documentation last year, and the latest documentation should be improved, with official Samples available:

The value of this variable is found to be related to the sliding direction through the test log:Copy the code
// Create a nestedScrollConnection to the nested scroll system to listen for the scroll in the LazyColumn
val nestedScrollConnection = remember {
            object : NestedScrollConnection {
                override fun onPreScroll(
                    available: Offset,
                    source: NestedScrollSource
                ): Offset {
                    val delta = available.y
                    oLog("scroll: delta:$delta")
                    // Add LazyColumn slip distance
                    val newOffset = slideOffsetHeightPx.value + delta
                    slideOffsetHeightPx.value = newOffset.coerceIn(-maxUpPx, minUpPx)
                    return Offset.Zero
                }
            }
}
// Parent slide list
Box(
    Modifier
   .fillMaxSize()
   .nestedScroll(nestedScrollConnection)
   ) {
            ...// List layout, such as LazyColumn
}
Copy the code

Then, knowing the distance of each step, you can get the cumulative sliding distance, and you can calculate the real-time sliding progress,

Then according to the progress and the maximum sliding distance of a part, the position or size of the part can be offset in real time:

Based on this, a simple encapsulation, adjust the control position according to the sliding progress, and adjust the control position here by dynamically setting it to offset.

Encapsulate files located in the project:

NestedWrapCustomLayout. Kt the Header section

Scrollableappbar. kt Header horizontal bar of information

Use:

/** * Personal center */
@Composable
fun ProfileContentScreen(
    coroutineScope: CoroutineScope,
    viewModel: ProfileViewModel,
    columnLazyState: LazyListState,
    profileData: DataProfile
) {
    Scaffold(
        modifier = Modifier
            .fillMaxSize()
    ) {
        var isShowDialog by remember { mutableStateOf(false)}val navController = LocalNavController.current
        // Encapsulate the sliding layout
        NestedWrapCustomLayout(
            columnTop = 202.dp,
            navigationIconSize = 80.dp,
            toolBarHeight = 56.dp,
            scrollableAppBarHeight = 202.dp,
            columnState = columnLazyState,
            scrollableAppBarBgColor = Color.LightGray,
            toolBar = { ProfileToolBar(profileData = profileData) },
            navigationIcon = { UserAdvertImg(advert = profileData.face) }, // Returns the icon by default
            extendUsrInfo = { UserNameUI(profileData = profileData) },
            headerTop = { HeaderTop() },
            backSlideProgress = { progress ->

            }
        ) {
            // There is a sliding layout space LazyListScope, so you can add items directly to the LazyColumn in the encapsulated layout

            /** 1
            item {
                BiliBanner(
                    modifier = Modifier
                        .fillMaxWidth()
                        .advancedShadow(
                            color = gray200,
                            alpha = 0.8 f,
                            shadowBlurRadius = 10.dp,
                            offsetX = 2.dp,
                            offsetY = 3.dp
                        ),
                    items = viewModel.bannerDataList,
                    config = BannerConfig(
                        indicatorColor = Color.White.copy(0.8 f),
                        selectedColor = bili_50.copy(0.8 f),
                        intervalTime = 3000
                    ),

                    itemOnClick = { banner ->
                        / / click on the banner
                        coroutineScope.launch {
                            NavUtil.doPageNavigationTo(
                                navController,
                                PageRoute.WEB_VIEW_ROUTE.replaceAfter("=", banner.url)
                            )
                        }
                    }
                )
            }

            /** 2. Advertising course */
            stickyHeader {
                ColumnStickHeader(title = "Your AD", subTitle = "These are all advertisements for the Flutter course.") } profileData.courseList? .let { item { CourseListView(courseList = it) } }/ /... Other projects}}Copy the code

3. Customize the dynamic effect of two-dimensional code scanning interface

Here I use Zxing and Camera, first introduce the dependency library:

    // Camera
    implementation "Androidx. Camera: the camera - camera2:1.0.2"
    implementation "Androidx. Camera: the camera - lifecycle: 1.0.2"
    implementation "Androidx. Camera: the camera - view: 1.0.0 - alpha31"
    // Zxing
    implementation 'com. Google. Zxing: core: 3.4.1 track'
Copy the code
  • First, you need to apply for camera permissions. Second, define an image parser that inherits the Camera library's ImageAnalysis.Analyzer and overwrites its analyze() method. The PlanarYUVLuminanceSource optimizes the YUV data returned by the camera driver by removing redundant data for accelerated decoding.Copy the code
class QrCodeAnalyzer(
    private val onQrCodeScanned: (String) -> Unit
) : ImageAnalysis.Analyzer {

    private val supportedImageFormats = listOf(
        ImageFormat.YUV_420_888,
        ImageFormat.YUV_422_888,
        ImageFormat.YUV_444_888,
    )

    override fun analyze(image: ImageProxy) {
        if (image.format in supportedImageFormats) {
            val bytes = image.planes.first().buffer.toByteArray()

            val source = PlanarYUVLuminanceSource(
                bytes,
                image.width,
                image.height,
                0.0,
                image.width,
                image.height,
                false
            )
            val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
            try {
                val result = MultiFormatReader().apply {
                    setHints(
                        mapOf(
                            DecodeHintType.POSSIBLE_FORMATS to arrayListOf(
                                BarcodeFormat.QR_CODE
                            )
                        )
                    )
                }.decode(binaryBitmap)
                // For callback results
                onQrCodeScanned(result.text)
            } catch (e: Exception) {
                e.printStackTrace()
            } finally {
                image.close()
            }
        }
    }

    private fun ByteBuffer.toByteArray(a): ByteArray {
        rewind()
        return ByteArray(remaining()).also {
            get(it)
        }
    }
}
Copy the code
  • AndroidView() is used for composing, which is itself a Compose:Copy the code
      AndroidView(
                modifier = Modifier.fillMaxSize(),
                factory = { context ->
                    val previewView = PreviewView(context)
                    val preview = Preview.Builder().build()
                    val selector = CameraSelector.Builder()
                        .requireLensFacing(CameraSelector.LENS_FACING_BACK)
                        .build()
                    preview.setSurfaceProvider(previewView.surfaceProvider)
                    val imageAnalysis = ImageAnalysis.Builder()
                        .setTargetResolution(
                            Size(previewView.width, previewView.height)
                        )
                        .setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST).build()
                    imageAnalysis.setAnalyzer(
                        ContextCompat.getMainExecutor(context),
                        QrCodeAnalyzer { result ->
                            // get the parse result
                            code = result
                        }
                    )

                    try {
                        cameraProviderFuture.get().bindToLifecycle(
                            lifecycleOwner,
                            selector,
                            preview,
                            imageAnalysis
                        )
                    } catch (e: Exception) {
                        e.printStackTrace()
                    }
                    previewView
                },
            )
Copy the code

The scan motion interface is also a Composable with animation that sets the background to transparent and sits on top of the camera to listen to zxing image parsing results.

Four, search page attention points

  • Search page mainly involves input box automatically get focus, page state management.Copy the code
  1. First define the keyboard manager, the focus request handler:
val keyboardController = LocalSoftwareKeyboardController.current
val focusRequester = FocusRequester()
Copy the code
  1. Next, the Modifier sets up the automatic focus request, passing in the focus request handler, and then onFocusChanged{} listens for focus changes and displays the soft keyboard if it is focused.
 TextField(
       modifier = Modifier
                .focusRequester(focusRequester)
                .onFocusChanged {
                    if(it.isFocused) { keyboardController? .show() } } .wrapContentHeight() .width(210.dp),
        value = inputValue,
        // Listen for input changes and actively invoke search
        onValueChange = {
            inputValue = it
            onSearch(inputValue)
        },
       / /...
     )
Copy the code
  1. Finally, the soft keyboard is triggered to hide when typing is complete:
keyboardActions = KeyboardActions(onSearch = { onSearch(inputValue) keyboardController? .hide() }),Copy the code

To sum up, the overall processing code of the search top input box is as follows:

/** * Search the input box ** /
@ExperimentalComposeUiApi
@Composable
fun SearchTopBar(
    needInputNow: String,
    onSearch: (String) - >Unit,
    onCancel: () -> Unit,
    onClearInput: () -> Unit
) {
    var inputValue by remember { mutableStateOf("")}if (needInputNow.isNotEmpty()) {
        inputValue = needInputNow
    }

    Row(
        modifier = Modifier
            .padding(horizontal = 16.dp)
            .fillMaxWidth()
            .height(48.dp)
            .background(color = Color.White, shape = RectangleShape),
        verticalAlignment = Alignment.CenterVertically
    ) {
        // To manage the soft keyboard
        val keyboardController = LocalSoftwareKeyboardController.current
        val focusRequester = FocusRequester()

        Row(
            modifier = Modifier
                .width(320.dp)
                .background(color = gray200, shape = RoundedCornerShape(24.dp))
                .padding(end = 8.dp, start = 16.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Icon(
                modifier = Modifier
                    .size(36.dp)
                    .padding(start = 8.dp),
                imageVector = Icons.Rounded.Search,
                contentDescription = "search",
                tint = Color.Gray
            )
            TextField(
                modifier = Modifier
                    .focusRequester(focusRequester)
                    .onFocusChanged {
                        if(it.isFocused) { keyboardController? .show() } } .wrapContentHeight() .width(210.dp),
                value = inputValue,
                onValueChange = {
                    inputValue = it
                    onSearch(inputValue)
                },
                placeholder = {
                    Text(
                        text = "Flour President",
                        style = TextStyle(fontSize = 14.sp)
                    )
                },
                colors = TextFieldDefaults.textFieldColors(
                    cursorColor = bili_50,
                    // Make all indicator state colors transparent
                    focusedIndicatorColor = Color.Transparent,
                    disabledIndicatorColor = Color.Transparent,
                    unfocusedIndicatorColor = Color.Transparent,
                    backgroundColor = gray200
                ),
                // Complete action custom processingkeyboardActions = KeyboardActions(onSearch = { onSearch(inputValue) keyboardController? .hide() }), textStyle = TextStyle(color = gray400),// a button is specified, such as Search, Done, etc
                keyboardOptions = KeyboardOptions.Default.copy(
                    imeAction = ImeAction.Search
                )
            )
            // Display the clear button if there is input text
            if (inputValue.isNotEmpty()) {
                IconButton(
                    modifier = Modifier
                        .padding(4.dp),
                    onClick = {
                        inputValue = ""
                        onClearInput()
                    },
                ) {
                    Icon(
                        modifier = Modifier
                            .size(28.dp)
                            .background(
                                color = gray300.copy(alpha = 0.3 f),
                                shape = CircleShape
                            ),
                        imageVector = Icons.Rounded.Clear,
                        contentDescription = "clear",
                        tint = gray400
                    )
                }
            }
        }

        DisposableEffect(Unit) {
            focusRequester.requestFocus()
            onDispose { }
        }

        TextButton(
            modifier = Modifier
                .padding(start = 4.dp)
                .wrapContentSize(),
            onClick = { onCancel() },
            contentPadding = PaddingValues(5.dp),
            colors = ButtonDefaults.textButtonColors(contentColor = Color.White)
        ) {
            Text(text = "Cancel", fontSize = 18.sp, color = Color.Gray)
        }

    }
}
Copy the code

V. Video playback

As for the video playback part, the previous article shared how to play the video in Compose. 【 References 】

In addition, list Paging loads use the Paging framework


Code structure:

  • Split more arbitrary, just in convenient search, according to the function of the small moduleCopy the code

Source code:

Github – Android Compose implements bili video playback

Github – Flutter enables bili video playback