首页 > 解决方案 > 在 Jetpack Compose 中使用自定义图像或文本样式实现可重用 Composable

问题描述

我目前正在开发两个 Android 项目,它们都使用 Jetpack Compose。这两个项目都使用了一个包含自定义组合的自行开发的库。

我现在的问题是我不知道如何在库中引用特定于项目的文本样式、图像或其他设计特征。不幸的是,Material Design 的实现是不够的。

举个例子:两个应用程序都应该有一个错误屏幕,以防出现问题。两个应用程序中的错误屏幕结构应该相同。错误屏幕应包含错误消息、错误图像和重试按钮。为了避免重复代码,我将可组合的错误屏幕移到了库中。那个时候没问题。

但错误屏幕也应该适应应用程序主题。目前,我在位于库模块内的可组合的错误屏幕中使用可绘制对象。按钮使用 MaterialTheme.typography.button 样式,消息使用 MaterialTheme.typography.body1 样式。

但这不是我想在这里实现的行为。我想为两个应用程序使用不同的错误图像,并且按钮和消息样式可能完全不同,具体取决于使用可组合的应用程序。

有什么聪明的方法可以实现这种行为吗?

编辑:我使用错误屏幕作为这个问题的一个易于理解的示例,但没有在库中实现它。但是我实现了一个可组合的配置文件,我面临着完全相同的问题。目前我正在使用该方法将文本样式作为参数传递,但这变得非常难看:

@Composable
fun Profile(
    modifier: Modifier = Modifier,
    user: User,
    sections: List<Section>,
    userIsLoggedIn: Boolean,
    onLogin: () -> Unit,
    onLogout: () -> Unit,
    loginButtonTextStyle: TextStyle = MaterialTheme.typography.button.copy(
        textAlign = TextAlign.Center
    ),
    logoutButtonTextStyle: TextStyle = MaterialTheme.typography.button.copy(
        color = MaterialTheme.colors.onSurface,
        textAlign = TextAlign.Center
    ),
    usernameTextStyle: TextStyle = MaterialTheme.typography.h5.copy(
        color = MaterialTheme.colors.onSurface,
        textAlign = TextAlign.Center
    ),
    sectionTitleTextStyle: TextStyle = MaterialTheme.typography.subtitle1.copy(
        color = MaterialTheme.colors.onSurface
    ),
    itemTitleTextStyle: TextStyle = MaterialTheme.typography.body1,
) {
    Column(
        modifier = modifier
            .verticalScroll(rememberScrollState())
            .padding(top = 24.dp)
    ) {
        if (userIsLoggedIn) {
            UsernameHeader(
                username = user.username,
                textStyle = usernameTextStyle
            )
        } else {
            LoginButton(
                onLogin = onLogin,
                textStyle = loginButtonTextStyle,
            )
        }

        SectionsContainer(
            sections = sections,
            sectionTitleTextStyle = sectionTitleTextStyle,
            itemTitleTextStyle = itemTitleTextStyle
        )

        if (userIsLoggedIn) {
            LogoutButton(
                onLogout = onLogout,
                textStyle = logoutButtonTextStyle
            )
        }
    }
}

@Composable
private fun UsernameHeader(
    username: String,
    textStyle: TextStyle
) {
    Text(
        text = "Hello $username",
        style = textStyle,
        modifier = Modifier
            .padding(horizontal = 16.dp)
            .fillMaxWidth()
    )
    Spacer(modifier = Modifier.height(32.dp))
}

@Composable
private fun LoginButton(
    textStyle: TextStyle,
    onLogin: () -> Unit
) {
    Button(
        onClick = onLogin,
        modifier = Modifier
            .padding(
                start = 16.dp,
                end = 16.dp,
            )
    ) {
        Text(
            text = "LOGIN",
            style = textStyle,
            modifier = Modifier
                .fillMaxWidth()
                .padding(top = 7.dp, bottom = 9.dp)
        )
    }
    Spacer(modifier = Modifier.height(24.dp))
}

@Composable
private fun LogoutButton(
    textStyle: TextStyle,
    onLogout: () -> Unit
) {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 32.dp)
    ) {
        TextButton(
            onClick = onLogout,
            modifier = Modifier
                .padding(
                    start = 24.dp,
                    end = 24.dp,
                    top = 7.dp,
                    bottom = 9.dp
                )
        ) {
            Text(
                text = "LOGOUT",
                style = textStyle,
            )
        }
    }
}

@Composable
private fun SectionsContainer(
    modifier: Modifier = Modifier,
    sections: List<Section>,
    sectionTitleTextStyle: TextStyle,
    itemTitleTextStyle: TextStyle
) {
    Column(modifier = modifier) {
        for (section in sections) {
            ProfileSection(
                section = section,
                titleTextStyle = sectionTitleTextStyle,
                itemTitleTextStyle = itemTitleTextStyle,
            )
        }
    }
}

@Composable
private fun ProfileSection(
    modifier: Modifier = Modifier,
    section: Section,
    titleTextStyle: TextStyle,
    itemTitleTextStyle: TextStyle,
) {
    Column(
        modifier.padding(bottom = 16.dp)
    ) {
        Text(
            text = section.title.toUpperCase(Locale.ROOT),
            style = titleTextStyle,
            modifier = Modifier
                .padding(
                    start = 16.dp,
                    end = 16.dp,
                    top = 9.dp,
                    bottom = 10.dp
                )
                .fillMaxWidth()
        )
        for (item in section.items) {
            ProfileItem(
                item = item,
                itemTitleTextStyle = itemTitleTextStyle
            )
        }
    }
}

@Composable
private fun ProfileItem(
    item: Item,
    itemTitleTextStyle: TextStyle
) {
    when (item) {
        is TextItem -> ProfileTextItem(
            textItem = item,
            titleTextStyle = itemTitleTextStyle
        )
        is ImageItem -> ProfileImageItem(imageItem = item)
    }
}

@Composable
private fun ProfileTextItem(
    textItem: TextItem,
    titleTextStyle: TextStyle
) {
    Text(
        text = textItem.title,
        style = titleTextStyle,
        modifier = Modifier
            .padding(
                start = 16.dp,
                end = 16.dp,
                top = 15.dp,
                bottom = 12.dp
            )
            .fillMaxWidth()
    )
}

@Composable
private fun ProfileImageItem(imageItem: ImageItem) {
    Image(
        painter = painterResource(id = imageItem.drawableResId),
        contentDescription = imageItem.contentDescription,
        modifier = Modifier.fillMaxWidth(),
        contentScale = ContentScale.FillWidth
    )
}

我使用的模型非常简单:

部分:

data class Section(
    val title: String,
    val items: List<Item>
)

物品:

interface Item

data class TextItem(
    val title: String
) : Item

data class ImageItem(
    val drawableResId: Int,
    val contentDescription: String?
) : Item

用户:

data class User(
    val username: String
)

然后,我在两个应用程序的活动中使用此配置文件可组合,如下所示:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val systemUiController = remember { SystemUiController(window) }
            CompositionLocalProvider(SystemUiControllerAmbient provides systemUiController) {
                SampleApp()
            }
        }
    }

    @Composable
    fun SampleApp() {
        SampleTheme {
            val systemUiController = SystemUiControllerAmbient.current
            systemUiController.setSystemBarsColor(color = MaterialTheme.colors.surface)

            Scaffold(
                modifier = Modifier.fillMaxSize(),
                topBar = { TopAppBarContainer() },
                bottomBar = { BottomNavigationContainer() }
            ) { innerPadding ->
                ProfileContainer(Modifier.padding(innerPadding))
            }
        }
    }

    @Composable
    private fun TopAppBarContainer() {
        TopAppBar(
            backgroundColor = MaterialTheme.colors.surface,
            contentColor = MaterialTheme.colors.onSurface
        ) {
            Text(
                text = "Profile",
                style = MaterialTheme.typography.h2,
                modifier = Modifier.padding(16.dp)
            )
        }
    }

    @Composable
    private fun ProfileContainer(modifier: Modifier = Modifier) {
        Surface(modifier = modifier) {
            var userIsLoggedIn by remember { mutableStateOf(false) }

            Profile(
                Modifier.fillMaxWidth(),
                user = User("Username"),
                sections = fakeSections,
                userIsLoggedIn = userIsLoggedIn,
                onLogin = { userIsLoggedIn = true },
                onLogout = { userIsLoggedIn = false }
            )
        }
    }

    @Composable
    private fun BottomNavigationContainer() {
        BottomNavigation(
            backgroundColor = MaterialTheme.colors.surface,
            contentColor = MaterialTheme.colors.onSurface,
        ) {
            BottomNavigationItem(
                selected = false,
                onClick = { },
                icon = {
                    Icon(
                        painter = painterResource(R.drawable.logo),
                        contentDescription = null,
                        modifier = Modifier.size(24.dp)
                    )
                },
                label = {
                    Text(
                        text = "Home",
                        style = MaterialTheme.typography.caption,
                        color = grey60
                    )
                }
            )
            BottomNavigationItem(
                selected = false,
                onClick = { },
                icon = {
                    Icon(
                        painter = painterResource(R.drawable.ic_assortment),
                        contentDescription = null,
                        modifier = Modifier.size(24.dp)
                    )
                },
                label = {
                    Text(
                        text = "Assortment",
                        style = MaterialTheme.typography.caption,
                        color = grey60
                    )
                }
            )
            BottomNavigationItem(
                selected = false,
                onClick = { },
                icon = {
                    Icon(
                        painter = painterResource(R.drawable.ic_heart),
                        contentDescription = null,
                        modifier = Modifier.size(24.dp)
                    )
                },
                label = {
                    Text(
                        text = "Wishlist",
                        style = MaterialTheme.typography.caption,
                        color = grey60
                    )
                }
            )
            BottomNavigationItem(
                selected = false,
                onClick = { },
                icon = {
                    Icon(
                        painter = painterResource(R.drawable.ic_cart),
                        contentDescription = null,
                        modifier = Modifier.size(24.dp)
                    )
                },
                label = {
                    Text(
                        text = "Cart",
                        style = MaterialTheme.typography.caption,
                        color = grey60
                    )
                }
            )
            BottomNavigationItem(
                selected = true,
                onClick = { },
                icon = {
                    Icon(
                        painter = painterResource(R.drawable.ic_user),
                        contentDescription = null,
                        tint = bonprixRed,
                        modifier = Modifier.size(24.dp)
                    )
                },
                label = {
                    Text(
                        text = "Profile",
                        style = MaterialTheme.typography.caption,
                        color = red
                    )
                }
            )
        }
    }

    @Preview
    @Composable
    fun DefaultPreview() {
        SampleApp()
    }
}

标签: material-components-androidandroid-jetpack-compose

解决方案


推荐阅读