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:
-
First, you define the page route, defining the initial route startDestination as the start page SplashScreen.
-
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.
-
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
- First define the keyboard manager, the focus request handler:
val keyboardController = LocalSoftwareKeyboardController.current
val focusRequester = FocusRequester()
Copy the code
- 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
- 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