基于Android實現(xiàn)工作管理甘特圖效果的代碼詳解
一、項目介紹
1.1 項目背景
在現(xiàn)代項目管理與團(tuán)隊協(xié)作中,甘特圖(Gantt Chart) 是最直觀的進(jìn)度可視化手段之一。它將項目拆分為若干任務(wù)(Task),以橫軸表示時間、縱軸表示任務(wù)序列,通過條狀圖(Bar)呈現(xiàn)每個任務(wù)的開始、結(jié)束和持續(xù)時長,幫助管理者一目了然地掌握項目進(jìn)度、資源分配與關(guān)鍵路徑。
在移動端,尤其是 Android 應(yīng)用場景,越來越多的團(tuán)隊管理、日程安排、考勤排班、生產(chǎn)計劃等應(yīng)用也需要在 App 內(nèi)展示甘特圖,以便移動辦公或現(xiàn)場管理。由于 Android 原生并無甘特圖組件,需要開發(fā)者自行實現(xiàn)或集成第三方庫。本項目目標(biāo)在不依賴重量級第三方庫的前提下,構(gòu)建一個高性能、靈活可定制、支持滾動縮放與交互的 Android 原生甘特圖組件,滿足以下需求:
任務(wù)條可視化:在甘特圖上繪制每個任務(wù)的條狀表示,并支持不同顏色、圖標(biāo)標(biāo)記。
時間軸刻度:橫軸顯示日期/小時刻度,并支持等級切換(日視圖/周視圖/月視圖)。
豎向滾動:任務(wù)過多時能上下滾動,自動復(fù)用行視圖減少內(nèi)存占用。
橫向滾動與縮放:時間跨度長時能左右滾動,并可通過手勢縮放時間軸(放大查看小時級細(xì)節(jié)/縮小看月級全局)。
任務(wù)交互:點擊任務(wù)彈出詳情,長按可拖拽調(diào)整開始/結(jié)束時間。
性能優(yōu)化:采用
RecyclerView
、Canvas
批繪、ViewHolder
復(fù)用等技術(shù),保證高幀率。可配置性:支持多種主題風(fēng)格(淺色/深色)、條高度、文字大小、行高、時間格式自定義。
MVVM 架構(gòu):前后端分離,數(shù)據(jù)由
ViewModel
管理,UI 僅關(guān)注渲染與交互。離線緩存:可將任務(wù)數(shù)據(jù)存儲于本地
Room
數(shù)據(jù)庫,實現(xiàn)離線展示與增量同步。
1.2 功能設(shè)計
功能模塊 | 說明 |
---|---|
時間軸刻度 | 支持日/周/月/季度四種視圖模式,并根據(jù)當(dāng)前縮放級別動態(tài)渲染刻度 |
任務(wù)列表 | 縱向顯示任務(wù)序列,使用 RecyclerView 實現(xiàn)可滾動、可復(fù)用 |
甘特條渲染 | 計算任務(wù)的開始/結(jié)束時間對應(yīng)的 X 坐標(biāo),在 Canvas 上繪制條形,支持自定義顏色 |
縮放與滾動 | 結(jié)合 ScaleGestureDetector 和 HorizontalScrollView ,實現(xiàn)平滑縮放和滾動 |
任務(wù)交互 | 點擊彈出 PopupWindow 顯示任務(wù)詳情;支持長按拖拽改變時間(高級功能可選) |
數(shù)據(jù)層 | 使用 Room 持久化任務(wù)數(shù)據(jù);ViewModel 暴露 LiveData<List<Task>> |
配置與主題 | 在 attrs.xml 定義可自定義屬性,如甘特條高度、顏色數(shù)組、時間格式等 |
1.3 技術(shù)選型
語言:Kotlin
UI:AndroidX、Material Components、ConstraintLayout
圖形繪制:Canvas + Paint + Path + PorterDuff(用于圖層混合,可用于復(fù)雜高亮)
手勢識別:
GestureDetector
+ScaleGestureDetector
列表復(fù)用:
RecyclerView
+LinearLayoutManager
數(shù)據(jù)持久化:Room + LiveData + ViewModel
協(xié)程:Kotlin Coroutines +
ViewModelScope
依賴注入:Hilt (可選)
日期處理:ThreeTenABP (
java.time
)
二、相關(guān)知識
2.1 Canvas 繪制原理
Canvas.drawRect/ drawRoundRect:繪制任務(wù)條;
Canvas.drawLine/ drawText:繪制刻度線和刻度文字;
圖層(saveLayer/ restore):在需要遮罩或混合模式時使用;
2.2 RecyclerView 性能優(yōu)化
ViewHolder 模式:復(fù)用任務(wù)行布局;
ItemDecoration:可用于繪制水平分隔線或輔助網(wǎng)格;
DiffUtil:高效計算數(shù)據(jù)變更并局部刷新;
2.3 手勢與視圖縮放
ScaleGestureDetector:監(jiān)聽雙指捏合手勢,實現(xiàn)縮放中心為手指焦點;
GestureDetector:監(jiān)聽單指滾動、雙擊等;
矩陣(Matrix):在 Canvas 平移與縮放時可用;
2.4 時間與坐標(biāo)映射
時間軸范圍:根據(jù)任務(wù)的最早開始和最晚結(jié)束計算總時長(毫秒);
像素映射:
x = (task.startTime - minTime) / timeSpan * totalWidth
;動態(tài)寬度:總寬度根據(jù)當(dāng)前縮放級別和屏幕寬度計算;
2.5 數(shù)據(jù)層與 MVVM
Room 實體:
@Entity data class Task(...)
;DAO:增刪改查和查詢?nèi)蝿?wù)列表;
ViewModel:使用
MutableLiveData<List<Task>>
管理任務(wù),協(xié)程異步加載;Activity/Fragment:觀察 LiveData 并將任務(wù)列表提交給適配器;
三、實現(xiàn)思路
總體框架
MainActivity
(或GanttChartFragment
)初始化 ViewModel、RecyclerView 與時間軸頭部;視圖分為兩部分:左側(cè)任務(wù)列表 + 右側(cè)甘特圖區(qū)域,后者可水平滾動;
使用嵌套
RecyclerView
:水平滾動用RecyclerView
+LinearLayoutManager(HORIZONTAL)
;或更輕量:右側(cè)放置一個自定義
GanttChartView
,外層套HorizontalScrollView
。
核心視圖:GanttChartView
繼承
View
,在onDraw()
中完成時間軸與任務(wù)條的繪制;支持
setTasks(List<Task>)
、setScale(scaleFactor: Float)
接口;維護(hù)
minTime
、maxTime
、timeSpan
、viewWidth
、rowHeight
、barHeight
等參數(shù)。
任務(wù)行復(fù)用
在
RecyclerView.Adapter
的onBindViewHolder()
中,將任務(wù)數(shù)據(jù)傳給GanttChartViewHolder
,后者調(diào)用ganttView.setTask(task)
并invalidate()
;GanttChartViewHolder
內(nèi)維護(hù)單個行高與索引,用以計算 Y 坐標(biāo)。
手勢縮放與滾動
在
GanttChartView
內(nèi)部實例化并注冊ScaleGestureDetector
,在onTouchEvent()
中轉(zhuǎn)發(fā),更新scaleFactor
并重新測量寬度后invalidate()
;外層
HorizontalScrollView
負(fù)責(zé)水平滾動;
點擊與拖拽(高級功能,可選)
監(jiān)聽
GestureDetector
的onSingleTapUp(event)
,計算點擊 X/Y 的時間和任務(wù)索引,彈出詳情對話框;長按后啟動拖拽,實時更新任務(wù)開始或結(jié)束時間并重繪。
時間刻度與視圖更新
在
GanttChartView.onDraw()
中先繪制頂部刻度行,循環(huán)for (i in 0..numTicks)
:
val x = leftPadding + i * (timeSpanPerTick / timeSpan) * viewWidth canvas.drawLine(x, 0f, x, headerHeight, axisPaint) canvas.drawText(formatTime(minTime + i * timeSpanPerTick), x, textY, textPaint)
- 下方依次繪制每個任務(wù)行的矩形條與任務(wù)名稱。
狀態(tài)管理與刷新
當(dāng)
scaleFactor
或任務(wù)列表更新時,調(diào)用ganttRecyclerView.adapter?.notifyDataSetChanged()
;可使用
DiffUtil
精細(xì)刷新;
四、整合代碼
以下將所有核心源文件與布局文件整合到同一代碼塊,用注釋區(qū)分文件,并附詳注釋。
// ---------------- 文件: build.gradle (Module) ---------------- /* plugins { id 'com.android.application' id 'kotlin-android' id 'kotlin-kapt' } android { compileSdkVersion 34 defaultConfig { applicationId "com.example.gantt" minSdkVersion 21 targetSdkVersion 34 versionCode 1 versionName "1.0" } buildFeatures { viewBinding true } } dependencies { implementation "androidx.core:core-ktx:1.10.1" implementation "androidx.appcompat:appcompat:1.7.0" implementation "com.google.android.material:material:1.9.0" implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.recyclerview:recyclerview:1.3.1" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1" implementation "androidx.room:room-runtime:2.5.2" kapt "androidx.room:room-compiler:2.5.2" implementation "org.threeten:threetenbp:1.6.0" // 或 ThreeTenABP implementation "com.jakewharton.threetenabp:threetenabp:1.4.4" } */ // ---------------- 文件: Task.kt ---------------- package com.example.gantt.data import androidx.room.Entity import androidx.room.PrimaryKey import org.threeten.bp.Instant import org.threeten.bp.ZonedDateTime /** * Task:Room 實體,表示甘特圖中的一個任務(wù) */ @Entity(tableName = "tasks") data class Task( @PrimaryKey(autoGenerate = true) val id: Long = 0, val name: String, val startTime: Long, // 毫秒時間戳 val endTime: Long, val color: Int // ARGB 顏色 ) // ---------------- 文件: TaskDao.kt ---------------- package com.example.gantt.data import androidx.lifecycle.LiveData import androidx.room.* /** * TaskDao:任務(wù)增刪改查接口 */ @Dao interface TaskDao { @Query("SELECT * FROM tasks ORDER BY startTime") fun getAllTasks(): LiveData<List<Task>> @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(task: Task) @Delete suspend fun delete(task: Task) } // ---------------- 文件: AppDatabase.kt ---------------- package com.example.gantt.data import androidx.room.Database import androidx.room.RoomDatabase /** * AppDatabase:Room 數(shù)據(jù)庫 */ @Database(entities = [Task::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun taskDao(): TaskDao } // ---------------- 文件: GanttViewModel.kt ---------------- package com.example.gantt.viewmodel import android.app.Application import androidx.lifecycle.* import androidx.room.Room import com.example.gantt.data.AppDatabase import com.example.gantt.data.Task import kotlinx.coroutines.launch /** * GanttViewModel:持有任務(wù)列表,提供增刪改查 */ class GanttViewModel(application: Application) : AndroidViewModel(application) { private val db = Room.databaseBuilder(application, AppDatabase::class.java, "gantt.db").build() private val dao = db.taskDao() val tasks: LiveData<List<Task>> = dao.getAllTasks() fun addTask(task: Task) = viewModelScope.launch { dao.insert(task) } fun deleteTask(task: Task) = viewModelScope.launch { dao.delete(task) } } // ---------------- 文件: activity_main.xml ---------------- <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- 左側(cè)任務(wù)名稱列表 --> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rvTasks" android:layout_width="120dp" android:layout_height="0dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"/> <!-- 右側(cè)甘特圖區(qū)域,水平滾動 --> <HorizontalScrollView android:id="@+id/scrollHorizontal" android:layout_width="0dp" android:layout_height="0dp" android:scrollbars="none" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@id/rvTasks" app:layout_constraintEnd_toEndOf="parent"> <com.example.gantt.ui.GanttChartView android:id="@+id/ganttView" android:layout_width="wrap_content" android:layout_height="match_parent"/> </HorizontalScrollView> <!-- 新增任務(wù)按鈕 --> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/fabAdd" android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@android:drawable/ic_input_add" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_margin="16dp"/> </androidx.constraintlayout.widget.ConstraintLayout> // ---------------- 文件: item_task_name.xml ---------------- <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/tvTaskName" android:layout_width="match_parent" android:layout_height="48dp" android:gravity="center_vertical" android:paddingStart="8dp" android:textSize="16sp" android:textColor="#333"/> // ---------------- 文件: TaskNameAdapter.kt ---------------- package com.example.gantt.ui import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.example.gantt.data.Task import com.example.gantt.databinding.ItemTaskNameBinding /** * TaskNameAdapter:左側(cè)任務(wù)名稱列表 */ class TaskNameAdapter : ListAdapter<Task, TaskNameAdapter.NameVH>(DIFF) { companion object { val DIFF = object : DiffUtil.ItemCallback<Task>() { override fun areItemsTheSame(old: Task, new: Task) = old.id == new.id override fun areContentsTheSame(old: Task, new: Task) = old == new } } inner class NameVH(val binding: ItemTaskNameBinding) : RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = NameVH(ItemTaskNameBinding.inflate(LayoutInflater.from(parent.context), parent, false)) override fun onBindViewHolder(holder: NameVH, position: Int) { holder.binding.tvTaskName.text = getItem(position).name } } // ---------------- 文件: GanttChartView.kt ---------------- package com.example.gantt.ui import android.content.Context import android.graphics.* import android.util.AttributeSet import android.view.* import android.widget.OverScroller import androidx.core.content.ContextCompat import com.example.gantt.R import com.example.gantt.data.Task import org.threeten.bp.Instant import org.threeten.bp.ZoneId /** * GanttChartView:自定義甘特圖 View */ class GanttChartView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ): View(context, attrs), GestureDetector.OnGestureListener, ScaleGestureDetector.OnScaleGestureListener { // 數(shù)據(jù) private var tasks: List<Task> = emptyList() private var minTime = Long.MAX_VALUE private var maxTime = 0L private var timeSpan = 1L // ms // 畫筆 private val barPaint = Paint(Paint.ANTI_ALIAS_FLAG) private val axisPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.GRAY; strokeWidth=2f } private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.DKGRAY; textSize = 24f } // 布局參數(shù) private val rowHeight = 80f private val headerHeight = 80f private var scaleFactor = 1f private var offsetX = 0f // 手勢 private val scroller = OverScroller(context) private val gestureDetector = GestureDetector(context, this) private val scaleDetector = ScaleGestureDetector(context, this) init { barPaint.style = Paint.Style.FILL } /** 外部設(shè)置任務(wù)并重新計算范圍 */ fun setTasks(list: List<Task>) { tasks = list if (tasks.isNotEmpty()) { minTime = tasks.minOf { it.startTime } maxTime = tasks.maxOf { it.endTime } timeSpan = maxTime - minTime } invalidate() } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) if (tasks.isEmpty()) return // 1. 繪制時間軸 val totalWidth = width.toFloat() * scaleFactor val tickCount = 6 for (i in 0..tickCount) { val x = offsetX + i / tickCount.toFloat() * totalWidth canvas.drawLine(x, 0f, x, headerHeight, axisPaint) val time = minTime + i / tickCount.toFloat() * timeSpan val label = Instant.ofEpochMilli(time) .atZone(ZoneId.systemDefault()).toLocalDate().toString() canvas.drawText(label, x + 10, headerHeight - 20, textPaint) } // 2. 繪制每行任務(wù)條 tasks.forEachIndexed { idx, task -> val top = headerHeight + idx * rowHeight val bottom = top + rowHeight * 0.6f // 計算左右 val left = offsetX + (task.startTime - minTime) / timeSpan.toFloat() * totalWidth val right = offsetX + (task.endTime - minTime) / timeSpan.toFloat() * totalWidth barPaint.color = task.color canvas.drawRect(left, top + 10, right, bottom, barPaint) } } // ================ 手勢與縮放 ================ override fun onTouchEvent(event: MotionEvent): Boolean { scaleDetector.onTouchEvent(event) if (!scaleDetector.isInProgress) { gestureDetector.onTouchEvent(event) } return true } override fun onScale(detector: ScaleGestureDetector): Boolean { scaleFactor *= detector.scaleFactor scaleFactor = scaleFactor.coerceIn(0.5f, 3f) invalidate() return true } override fun onScaleBegin(detector: ScaleGestureDetector) = true override fun onScaleEnd(detector: ScaleGestureDetector) {} override fun onDown(e: MotionEvent) = true override fun onShowPress(e: MotionEvent) {} override fun onSingleTapUp(e: MotionEvent) = false override fun onScroll(e1: MotionEvent, e2: MotionEvent, dx: Float, dy: Float): Boolean { offsetX -= dx offsetX = offsetX.coerceIn(-width.toFloat(), width.toFloat() * scaleFactor) invalidate() return true } override fun onLongPress(e: MotionEvent) {} override fun onFling(e1: MotionEvent, e2: MotionEvent, vx: Float, vy: Float): Boolean { scroller.fling( offsetX.toInt(), 0, vx.toInt(), 0, (-width).toInt(), (width * scaleFactor).toInt(), 0, 0 ) postInvalidateOnAnimation() return true } override fun computeScroll() { if (scroller.computeScrollOffset()) { offsetX = scroller.currX.toFloat() invalidate() } } } // ---------------- 文件: MainActivity.kt ---------------- package com.example.gantt.ui import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager import com.example.gantt.databinding.ActivityMainBinding import com.example.gantt.data.Task import com.example.gantt.viewmodel.GanttViewModel import org.threeten.bp.ZonedDateTime /** * MainActivity:示例甘特圖展示 */ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private val vm: GanttViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) // 左側(cè)任務(wù)名列表 val nameAdapter = TaskNameAdapter() binding.rvTasks.apply { layoutManager = LinearLayoutManager(this@MainActivity) adapter = nameAdapter } // 觀察任務(wù)數(shù)據(jù) vm.tasks.observe(this) { list -> nameAdapter.submitList(list) binding.ganttView.setTasks(list) } // 新增示例任務(wù) binding.fabAdd.setOnClickListener { val now = System.currentTimeMillis() val task = Task( name = "任務(wù)${now%100}", startTime = now, endTime = now + 3600_000 * (1 + (now%5).toInt()), color = android.graphics.Color.rgb(((now/1000)%255).toInt(),120,150) ) vm.addTask(task) } } } // ---------------- 文件: activity_main.xml (ViewBinding) ---------------- <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data/> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rvTasks" android:layout_width="120dp" android:layout_height="0dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"/> <HorizontalScrollView android:id="@+id/scrollHorizontal" android:layout_width="0dp" android:layout_height="0dp" android:scrollbars="none" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@id/rvTasks" app:layout_constraintEnd_toEndOf="parent"> <com.example.gantt.ui.GanttChartView android:id="@+id/ganttView" android:layout_width="2000dp" android:layout_height="match_parent"/> </HorizontalScrollView> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/fabAdd" android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@android:drawable/ic_input_add" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_margin="16dp"/> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
五、方法說明
TaskDao.getAllTasks():異步獲取任務(wù)列表并以
LiveData
形式暴露,自動監(jiān)聽數(shù)據(jù)變化;GanttViewModel.addTask()/deleteTask():在
ViewModelScope
中執(zhí)行 Room 操作,保證 UI 線程不被阻塞;TaskNameAdapter:左側(cè)垂直列表,僅負(fù)責(zé)顯示任務(wù)名稱;
GanttChartView.setTasks(list):接受任務(wù)列表,計算
minTime
、maxTime
、timeSpan
并刷新視圖;GanttChartView.onDraw():先繪制頂部時間刻度,再遍歷任務(wù)列表繪制每個任務(wù)條;
GestureDetector.OnGestureListener 與 ScaleGestureDetector.OnScaleGestureListener:分別響應(yīng)單指滾動以平移視圖、雙指縮放以調(diào)整
scaleFactor
;OverScroller:在
onFling()
中啟動慣性滑動,并在computeScroll()
連續(xù)更新offsetX
;MainActivity:
綁定
RecyclerView
與GanttChartView
;觀察
ViewModel.tasks
,雙向提交數(shù)據(jù);點擊
fabAdd
隨機(jī)新增任務(wù)演示效果。
六、項目總結(jié)
6.1 成果回顧
完成了一個原生 Android 甘特圖組件,支持縱向任務(wù)列表、橫向時間軸、自動計算坐標(biāo)與自適應(yīng)縮放;
采用
RecyclerView
與純View
繪制相結(jié)合,實現(xiàn)高性能渲染與交互;支持手勢縮放、滾動與慣性滑動,用戶體驗流暢;
數(shù)據(jù)層基于 Room + LiveData + ViewModel,實現(xiàn)離線存儲與實時刷新。
6.2 技術(shù)收獲
深入理解了 Canvas 坐標(biāo)映射、時間→像素轉(zhuǎn)換與自定義 View 繪制機(jī)制;
掌握了
GestureDetector
、ScaleGestureDetector
、OverScroller
等手勢與慣性滑動 API;學(xué)會在 MVVM 架構(gòu)中整合 Room 數(shù)據(jù)庫與 UI 組件;
學(xué)習(xí)了如何在 Android 上實現(xiàn)可配置、高性能的大數(shù)據(jù)量可視化組件。
6.3 后續(xù)優(yōu)化
動態(tài)加載:對超大時間跨度任務(wù),按需加載時間刻度與任務(wù)條,避免一次性繪制過多元素;
任務(wù)交互:添加任務(wù)拖拽改變起止時間、滑動調(diào)整時長、長按彈出上下文菜單;
視圖聯(lián)動:任務(wù)列表與甘特圖聯(lián)動,點擊任務(wù)名高亮甘特條,點擊甘特條滾動列表;
主題與樣式:支持深色模式、可定制行高、條高度、刻度字體、間隔顏色等;
性能檢測:使用 Systrace 分析繪制與手勢響應(yīng),進(jìn)一步優(yōu)化幀率;
無障礙:為甘特條和時間刻度添加
contentDescription
,提升 A11Y 體驗;單元測試與 UI 自動化測試:重點測試時間映射、縮放邏輯與滑動邊界。
以上就是基于Android實現(xiàn)工作管理甘特圖效果的代碼詳解的詳細(xì)內(nèi)容,更多關(guān)于Android工作管理甘特圖的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
安卓應(yīng)用開發(fā)通過java調(diào)用c++ jni的圖文使用方法
這篇文章主要介紹了2013-11-11Android編程實現(xiàn)Home鍵的屏蔽,捕獲與修改方法
這篇文章主要介紹了Android編程實現(xiàn)Home鍵的屏蔽,捕獲與修改方法,實例分析了使用onAttachedToWindow捕獲Home鍵的相關(guān)技巧,需要的朋友可以參考下2016-06-06Flutter路由的跳轉(zhuǎn)、動畫和傳參詳解(最簡單)
這篇文章主要給大家介紹了關(guān)于Flutter路由的跳轉(zhuǎn)、動畫和傳參的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-01-01Android實現(xiàn)仿網(wǎng)易新聞的頂部導(dǎo)航指示器
這篇文章主要介紹了Android實現(xiàn)仿網(wǎng)易新聞的頂部導(dǎo)航指示器的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-08-08Android App的運(yùn)行環(huán)境及Android系統(tǒng)架構(gòu)概覽
這篇文章主要介紹了Android App的運(yùn)行環(huán)境及Android系統(tǒng)架構(gòu)概覽,并對應(yīng)用程序進(jìn)程間隔離機(jī)制等知識點作了介紹,需要的朋友可以參考下2016-03-03