欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

基于Android實現(xiàn)工作管理甘特圖效果的代碼詳解

 更新時間:2025年05月07日 09:13:08   作者:Katie。  
在現(xiàn)代項目管理與團(tuán)隊協(xié)作中,甘特圖(Gantt?Chart)?是最直觀的進(jìn)度可視化手段之一,在移動端,尤其是?Android?應(yīng)用場景,越來越多的日程安排、考勤排班等應(yīng)用也需要在?App?內(nèi)展示甘特圖,以便移動辦公或現(xiàn)場管理,所以本文介紹了基于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)思路

  1. 總體框架

    • MainActivity(或 GanttChartFragment)初始化 ViewModel、RecyclerView 與時間軸頭部;

    • 視圖分為兩部分:左側(cè)任務(wù)列表 + 右側(cè)甘特圖區(qū)域,后者可水平滾動;

    • 使用嵌套 RecyclerView:水平滾動用 RecyclerView + LinearLayoutManager(HORIZONTAL)

    • 或更輕量:右側(cè)放置一個自定義 GanttChartView,外層套 HorizontalScrollView。

  2. 核心視圖:GanttChartView

    • 繼承 View,在 onDraw() 中完成時間軸與任務(wù)條的繪制;

    • 支持 setTasks(List<Task>)、setScale(scaleFactor: Float) 接口;

    • 維護(hù) minTime、maxTimetimeSpan、viewWidthrowHeight、barHeight 等參數(shù)。

  3. 任務(wù)行復(fù)用

    • 在 RecyclerView.Adapter 的 onBindViewHolder() 中,將任務(wù)數(shù)據(jù)傳給 GanttChartViewHolder,后者調(diào)用 ganttView.setTask(task) 并 invalidate();

    • GanttChartViewHolder 內(nèi)維護(hù)單個行高與索引,用以計算 Y 坐標(biāo)。

  4. 手勢縮放與滾動

    • 在 GanttChartView 內(nèi)部實例化并注冊 ScaleGestureDetector,在 onTouchEvent() 中轉(zhuǎn)發(fā),更新 scaleFactor 并重新測量寬度后 invalidate()

    • 外層 HorizontalScrollView 負(fù)責(zé)水平滾動;

  5. 點擊與拖拽(高級功能,可選)

    • 監(jiān)聽 GestureDetector 的 onSingleTapUp(event),計算點擊 X/Y 的時間和任務(wù)索引,彈出詳情對話框;

    • 長按后啟動拖拽,實時更新任務(wù)開始或結(jié)束時間并重繪。

  6. 時間刻度與視圖更新

    • 在 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、maxTimetimeSpan 并刷新視圖;

  • GanttChartView.onDraw():先繪制頂部時間刻度,再遍歷任務(wù)列表繪制每個任務(wù)條;

  • GestureDetector.OnGestureListener 與 ScaleGestureDetector.OnScaleGestureListener:分別響應(yīng)單指滾動以平移視圖、雙指縮放以調(diào)整 scaleFactor

  • OverScroller:在 onFling() 中啟動慣性滑動,并在 computeScroll() 連續(xù)更新 offsetX ;

  • MainActivity

    1. 綁定 RecyclerView 與 GanttChartView

    2. 觀察 ViewModel.tasks,雙向提交數(shù)據(jù);

    3. 點擊 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)文章

最新評論