深入探索 Compose 的高级特性,通过完整实战项目掌握企业级开发技能。
前言
经过前六篇的学习,我们已经掌握了 Compose 的核心开发技能。本篇文章将探索 Compose 的高级特性,并通过一个完整的实战项目,将所学知识融会贯通。
一、自定义绘制
1.1 Canvas 基础
Canvas 是 Compose 中自定义绘制的核心组件:
@Composable
fun CustomCanvas() {
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
) {
// 绘制矩形
drawRect(
color = Color.Blue,
topLeft = Offset(0f, 0f),
size = Size(100f, 100f)
)
// 绘制圆形
drawCircle(
color = Color.Red,
radius = 50f,
center = Offset(200f, 100f)
)
// 绘制线条
drawLine(
color = Color.Green,
start = Offset(0f, 200f),
end = Offset(size.width, 200f),
strokeWidth = 5f
)
}
}
1.2 Path 绘制
@Composable
fun TriangleShape() {
Canvas(modifier = Modifier.size(100.dp)) {
val path = Path().apply {
moveTo(size.width / 2, 0f)
lineTo(size.width, size.height)
lineTo(0f, size.height)
close()
}
drawPath(
path = path,
color = Color.Blue
)
}
}
@Composable
fun CurvedLine() {
Canvas(modifier = Modifier.fillMaxWidth().height(200.dp)) {
val path = Path().apply {
moveTo(0f, size.height / 2)
// 二次贝塞尔曲线
quadraticBezierTo(
size.width / 2, 0f,
size.width, size.height / 2
)
}
drawPath(
path = path,
color = Color.Red,
style = Stroke(width = 5f)
)
}
}
1.3 渐变绘制
@Composable
fun GradientBackground() {
Canvas(modifier = Modifier.fillMaxSize()) {
val gradient = Brush.linearGradient(
colors = listOf(Color.Blue, Color.Purple, Color.Red),
start = Offset(0f, 0f),
end = Offset(size.width, size.height)
)
drawRect(brush = gradient)
}
}
@Composable
fun RadialGradientCircle() {
Canvas(modifier = Modifier.size(200.dp)) {
val gradient = Brush.radialGradient(
colors = listOf(Color.Yellow, Color.Orange, Color.Red),
center = center,
radius = size.minDimension / 2
)
drawCircle(brush = gradient)
}
}
1.4 实战:饼图组件
@Composable
fun PieChart(
data: List<PieData>,
modifier: Modifier = Modifier
.size(200.dp)
) {
val total = data.sumOf { it.value.toDouble() }.toFloat()
var startAngle = -90f
Canvas(modifier = modifier) {
data.forEach { item ->
val sweepAngle = (item.value / total) * 360f
drawArc(
color = item.color,
startAngle = startAngle,
sweepAngle = sweepAngle,
useCenter = true
)
startAngle += sweepAngle
}
}
}
data class PieData(
val value: Float,
val color: Color,
val label: String
)
二、图形变换
2.1 GraphicsLayer
@Composable
fun TransformedBox() {
var rotation by remember { mutableStateOf(0f) }
var scale by remember { mutableStateOf(1f) }
Box(
modifier = Modifier
.graphicsLayer {
rotationZ = rotation
scaleX = scale
scaleY = scale
// 阴影
shadowElevation = 10f
// 裁剪
clip = true
shape = RoundedCornerShape(16.dp)
}
.size(100.dp)
.background(Color.Blue)
.clickable {
rotation += 45f
scale = if (scale == 1f) 1.5f else 1f
}
)
}
2.2 BlendMode 混合模式
@Composable
fun BlendModeDemo() {
Canvas(modifier = Modifier.fillMaxSize()) {
// 绘制红色圆形
drawCircle(
color = Color.Red,
radius = 100f,
center = Offset(size.width / 2 - 50, size.height / 2)
)
// 使用混合模式绘制蓝色圆形
drawCircle(
color = Color.Blue,
radius = 100f,
center = Offset(size.width / 2 + 50, size.height / 2),
blendMode = BlendMode.Multiply // 正片叠底
)
}
}
三、窗口 Insets 处理
3.1 什么是 Insets?
Insets 表示系统 UI 占据的区域:
- 状态栏(Status Bar)
- 导航栏(Navigation Bar)
- 键盘(IME)
- 刘海屏/挖孔屏(Display Cutout)
3.2 WindowInsets API
@Composable
fun InsetsDemo() {
// 获取各种 Insets
val statusBars = WindowInsets.statusBars
val navigationBars = WindowInsets.navigationBars
val ime = WindowInsets.ime
val safeDrawing = WindowInsets.safeDrawing
// 应用到布局
Box(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(statusBars)
.windowInsetsPadding(navigationBars)
) {
// 内容不会被系统栏遮挡
}
}
3.3 边缘到边缘适配
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 启用边缘到边缘
enableEdgeToEdge()
setContent {
MyApp()
}
}
}
@Composable
fun EdgeToEdgeScreen() {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = { Text("标题") },
modifier = Modifier.windowInsetsPadding(WindowInsets.statusBars)
)
},
bottomBar = {
NavigationBar(
modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars)
) {
// 导航项
}
}
) { innerPadding ->
// 内容区域自动避开系统栏
LazyColumn(
modifier = Modifier.padding(innerPadding)
) {
// 列表内容
}
}
}
3.4 键盘处理
@Composable
fun KeyboardAwareScreen() {
val imeInsets = WindowInsets.ime
val isKeyboardVisible = imeInsets.getBottom(LocalDensity.current) > 0
Column(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.statusBars)
) {
// 内容区域
LazyColumn(
modifier = Modifier.weight(1f)
) {
// 列表项
}
// 输入框
OutlinedTextField(
value = text,
onValueChange = { text = it },
modifier = Modifier
.fillMaxWidth()
.windowInsetsPadding(WindowInsets.ime)
)
}
}
四、实战项目:新闻阅读 App
4.1 项目架构
app/
├── data/
│ ├── remote/
│ │ ├── NewsApi.kt
│ │ └── dto/
│ ├── local/
│ │ ├── NewsDao.kt
│ │ └── AppDatabase.kt
│ ├── repository/
│ │ └── NewsRepository.kt
│ └── model/
│ └── Article.kt
├── ui/
│ ├── theme/
│ │ ├── Color.kt
│ │ ├── Theme.kt
│ │ └── Type.kt
│ ├── component/
│ │ ├── ArticleCard.kt
│ │ ├── LoadingIndicator.kt
│ │ └── ErrorMessage.kt
│ ├── screen/
│ │ ├── home/
│ │ │ ├── HomeScreen.kt
│ │ │ ├── HomeViewModel.kt
│ │ │ └── HomeUiState.kt
│ │ ├── detail/
│ │ │ ├── DetailScreen.kt
│ │ │ └── DetailViewModel.kt
│ │ └── bookmark/
│ │ └── BookmarkScreen.kt
│ └── navigation/
│ └── AppNavigation.kt
└── di/
└── AppModule.kt
4.2 数据层实现
// 数据模型
data class Article(
val id: String,
val title: String,
val summary: String,
val imageUrl: String,
val publishedAt: String,
val isBookmarked: Boolean = false
)
// API 接口
interface NewsApi {
@GET("top-headlines")
suspend fun getTopHeadlines(
@Query("country") country: String = "us",
@Query("apiKey") apiKey: String = BuildConfig.API_KEY
): NewsResponse
@GET("everything")
suspend fun searchNews(
@Query("q") query: String,
@Query("apiKey") apiKey: String = BuildConfig.API_KEY
): NewsResponse
}
// 数据库
@Dao
interface NewsDao {
@Query("SELECT * FROM articles ORDER BY publishedAt DESC")
fun getAllArticles(): Flow<List<ArticleEntity>>
@Query("SELECT * FROM articles WHERE isBookmarked = 1")
fun getBookmarkedArticles(): Flow<List<ArticleEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertArticles(articles: List<ArticleEntity>)
@Query("UPDATE articles SET isBookmarked = :isBookmarked WHERE id = :articleId")
suspend fun updateBookmark(articleId: String, isBookmarked: Boolean)
}
// Repository
class NewsRepository @Inject constructor(
private val api: NewsApi,
private val dao: NewsDao
) {
fun getArticles(): Flow<List<Article>> = flow {
try {
val response = api.getTopHeadlines()
val articles = response.articles.map { it.toDomain() }
dao.insertArticles(articles.map { it.toEntity() })
} catch (e: Exception) {
// 网络失败时使用本地缓存
}
emitAll(dao.getAllArticles().map { list ->
list.map { it.toDomain() }
})
}
suspend fun toggleBookmark(articleId: String, isBookmarked: Boolean) {
dao.updateBookmark(articleId, isBookmarked)
}
}
4.3 ViewModel 实现
sealed interface HomeUiState {
data object Loading : HomeUiState
data class Success(val articles: List<Article>) : HomeUiState
data class Error(val message: String) : HomeUiState
}
@HiltViewModel
class HomeViewModel @Inject constructor(
private val repository: NewsRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<HomeUiState>(HomeUiState.Loading)
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
init {
loadArticles()
}
private fun loadArticles() {
viewModelScope.launch {
repository.getArticles()
.onStart { _uiState.value = HomeUiState.Loading }
.catch { e ->
_uiState.value = HomeUiState.Error(e.message ?: "Unknown error")
}
.collect { articles ->
_uiState.value = HomeUiState.Success(articles)
}
}
}
fun toggleBookmark(articleId: String, isBookmarked: Boolean) {
viewModelScope.launch {
repository.toggleBookmark(articleId, isBookmarked)
}
}
fun refresh() {
loadArticles()
}
}
4.4 UI 实现
@Composable
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel(),
onArticleClick: (String) -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text("新闻") },
actions = {
IconButton(onClick = { viewModel.refresh() }) {
Icon(Icons.Default.Refresh, contentDescription = "刷新")
}
}
)
}
) { innerPadding ->
when (val state = uiState) {
is HomeUiState.Loading -> LoadingIndicator()
is HomeUiState.Error -> ErrorMessage(
message = state.message,
onRetry = { viewModel.refresh() }
)
is HomeUiState.Success -> {
ArticleList(
articles = state.articles,
onArticleClick = onArticleClick,
onBookmarkClick = { id, bookmarked ->
viewModel.toggleBookmark(id, bookmarked)
},
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
@Composable
fun ArticleList(
articles: List<Article>,
onArticleClick: (String) -> Unit,
onBookmarkClick: (String, Boolean) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(articles, key = { it.id }) { article ->
ArticleCard(
article = article,
onClick = { onArticleClick(article.id) },
onBookmarkClick = { onBookmarkClick(article.id, !article.isBookmarked) }
)
}
}
}
@Composable
fun ArticleCard(
article: Article,
onClick: () -> Unit,
onBookmarkClick: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
onClick = onClick
) {
Column {
AsyncImage(
model = article.imageUrl,
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
contentScale = ContentScale.Crop
)
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = article.title,
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = article.summary,
style = MaterialTheme.typography.bodyMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = article.publishedAt,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
IconButton(onClick = onBookmarkClick) {
Icon(
imageVector = if (article.isBookmarked) {
Icons.Default.Bookmark
} else {
Icons.Default.BookmarkBorder
},
contentDescription = "收藏"
)
}
}
}
}
}
}
4.5 导航配置
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(
onArticleClick = { articleId ->
navController.navigate("detail/$articleId")
}
)
}
composable(
route = "detail/{articleId}",
arguments = listOf(
navArgument("articleId") { type = NavType.StringType }
)
) { backStackEntry ->
val articleId = backStackEntry.arguments?.getString("articleId") ?: ""
DetailScreen(
articleId = articleId,
onBack = { navController.popBackStack() }
)
}
composable("bookmarks") {
BookmarkScreen()
}
}
}
五、性能优化实战
5.1 列表优化
@Composable
fun OptimizedArticleList(articles: List<Article>) {
LazyColumn {
items(
items = articles,
key = { it.id }, // 使用 key 优化重组
contentType = { it::class } // 使用 contentType 优化复用
) { article ->
ArticleCard(
article = article,
// 缓存 lambda 避免每次重组创建
onClick = remember(article.id) {
{ viewModel.onArticleClick(article.id) }
}
)
}
}
}
5.2 图片优化
@Composable
fun OptimizedImage(imageUrl: String) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
.crossfade(true)
.placeholder(R.drawable.placeholder)
.error(R.drawable.error)
// 内存缓存
.memoryCachePolicy(CachePolicy.ENABLED)
// 磁盘缓存
.diskCachePolicy(CachePolicy.ENABLED)
// 图片尺寸
.size(800, 600)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth()
)
}
六、本篇小结
通过本篇文章,我们:
- 掌握了自定义绘制:Canvas、Path、渐变绘制
- 理解了图形变换:GraphicsLayer、BlendMode
- 学会了 Insets 处理:边缘到边缘适配、键盘处理
- 完成了实战项目:新闻阅读 App 的完整实现
- 实践了性能优化:列表优化、图片优化
系列总结
经过七篇的学习,我们系统地掌握了 Jetpack Compose:
| 篇章 | 核心内容 |
|---|---|
| 第一篇 | 环境搭建、Composable 基础、Material 组件入门 |
| 第二篇 | Modifier 原理、布局系统、LazyColumn、ConstraintLayout |
| 第三篇 | Slot Table、状态管理、ViewModel、副作用 API |
| 第四篇 | Material Design 3、主题定制、深色模式、动态取色 |
| 第五篇 | 动画系统、手势处理、自定义动画 |
| 第六篇 | 项目架构、Navigation、Hilt、性能优化 |
| 第七篇 | 自定义绘制、Insets 处理、实战项目 |
希望这个系列能帮助你真正精通 Compose!
如果本系列对你有帮助,欢迎 点赞、收藏、关注!有任何问题可以在评论区留言。