Detailed explanation of the code to implement work management Gantt chart effects based on Android
1. Project introduction
1.1 Project background
In modern project management and team collaboration,Gantt ChartIt is one of the most intuitive methods of progress visualization. It splits the project into several tasks (tasks), uses the horizontal axis to represent time and the vertical axis to represent task sequences. It presents the beginning, end and duration of each task through a bar chart (Bar), helping managers to understand the project progress, resource allocation and key paths at a glance.
In mobile, especially Android application scenarios, more and more applications such as team management, schedule, attendance scheduling, production planning, etc. also need to display Gantt charts in the App for mobile office or on-site management. Since Android does not have Gantt Chart components natively, developers need to implement or integrate third-party libraries themselves. The goal of this project is to build a high-performance, flexible and customizable, support scrolling scaling and interaction without relying on heavyweight third-party libraries.Android native Gantt Chart Components, meet the following needs:
Taskbar visualization: Draw the bar representation of each task on the Gantt chart and support different colors and icon marks.
Timeline scale: The horizontal axis displays the date/hour scale and supports level switching (daily view/weekly view/monthly view).
Scroll vertically: When there are too many tasks, you can scroll up and down, and automatically reuse the row view to reduce memory usage.
Horizontal scrolling and zooming: When the time span is long, it can scroll left and right, and the timeline can be scaled through gestures (zoom in to view hourly details/zoom in to see the global monthly level).
Task interaction: Click on the task to pop up details, press and drag to adjust the start/end time.
Performance optimization:Used
RecyclerView
、Canvas
Batch drawing,ViewHolder
Technology such as multiplexing ensures high frame rate.Configurability: Supports customization of various theme styles (light/dark), bar height, text size, line height, and time format.
MVVM architecture: The front and back ends are separated, and the data is
ViewModel
Management, UI only focuses on rendering and interaction.Offline cache: The task data can be stored locally
Room
Database, realize offline display and incremental synchronization.
1.2 Functional design
Functional module | illustrate |
---|---|
Timeline scale | Supports four view modes: day/week/month/quarterly, and dynamically renders the scale according to the current zoom level |
Task List | Display task sequence vertically, usingRecyclerView Realize rollable and reusable |
Gantt strip rendering | Calculate the X coordinates corresponding to the start/end time of the task, draw bars on Canvas, and support custom colors |
Zoom and scroll | CombinedScaleGestureDetector andHorizontalScrollView , achieve smooth scaling and scrolling |
Task interaction | Click to pop upPopupWindow Display task details; support long press and drag to change the time (advanced functions are optional) |
Data layer | useRoom Persistence task data;ViewModel ExposedLiveData<List<Task>>
|
Configuration and Theme | exist Define customizable properties, such as Gantt bar height, color array, time format, etc. |
1.3 Technical selection
language:Kotlin
UI:AndroidX、Material Components、ConstraintLayout
Graphic drawing: Canvas + Paint + Path + PorterDuff (for layer mixing, can be used for complex highlighting)
Gesture recognition:
GestureDetector
+ScaleGestureDetector
List reuse:
RecyclerView
+LinearLayoutManager
Data persistence:Room + LiveData + ViewModel
Coroutine:Kotlin Coroutines +
ViewModelScope
Dependency injection: Hilt (optional)
Date processing:ThreeTenABP (
)
2. Related knowledge
2.1 Canvas drawing principle
/ drawRoundRect: Draw task bars;
/ drawText: Draw ticks and tick text;
layer (saveLayer/restore): Used when masking or blending mode is required;
2.2 RecyclerView Performance Optimization
ViewHolder Mode: Reuse task line layout;
ItemDecoration: Can be used to draw horizontal dividers or auxiliary grids;
DiffUtil: Efficiently calculate data changes and partial refresh;
2.3 Gestures and View Zoom
ScaleGestureDetector: Listen to the pinch gesture of the two fingers to realize the zoom center as the focus of the finger;
GestureDetector: Listen to single finger scrolling, double-clicking, etc.;
Matrix: Available when Canvas pan and zoom;
2.4 Time and coordinate mapping
Timeline range: Calculate the total duration (milliseconds) based on the earliest start and latest end of the task;
Pixel Mapping:
x = ( - minTime) / timeSpan * totalWidth
;Dynamic width: The total width is calculated based on the current zoom level and screen width;
2.5 Data layer and MVVM
Room entity:
@Entity data class Task(...)
;DAO: Add, delete, modify and query task list;
ViewModel:use
MutableLiveData<List<Task>>
Manage tasks, coroutines are loaded asynchronously;Activity/Fragment: Observe LiveData and submit the task list to the adapter;
Ideas for realization
-
Overall framework
MainActivity
(orGanttChartFragment
) Initialize ViewModel, RecyclerView and timeline header;The view is divided into two parts:Task list on the left + Gantt chart area on the right, the latter can scroll horizontally;
Using nesting
RecyclerView
: Use horizontal scrollingRecyclerView
+LinearLayoutManager(HORIZONTAL)
;Or lighter: place a custom one on the right
GanttChartView
, outer coverHorizontalScrollView
。
-
Core View: GanttChartView
Inheritance
View
,existonDraw()
Complete the drawing of the timeline and task bars;support
setTasks(List<Task>)
、setScale(scaleFactor: Float)
Interface;Maintenance
minTime
、maxTime
、timeSpan
、viewWidth
、rowHeight
、barHeight
etc.
-
Task line reuse
exist
of
onBindViewHolder()
, pass the task data toGanttChartViewHolder
, the latter call(task)
andinvalidate()
;GanttChartViewHolder
Maintain a single row height and index within to calculate the Y coordinates.
-
Gesture zoom and scroll
exist
GanttChartView
Internal instantiation and registrationScaleGestureDetector
,existonTouchEvent()
Forward, updatescaleFactor
After remeasurement of the widthinvalidate()
;Outer layer
HorizontalScrollView
Responsible for horizontal rolling;
-
Click and drag(Advanced features, optional)
Monitor
GestureDetector
ofonSingleTapUp(event)
, calculate the time and task index of clicking X/Y, and a details dialog box pops up;Press and start dragging, update the task start or end time in real time and repaint it.
-
Time scale and view update
exist
()
First draw the top scale row, loopfor (i in 0..numTicks)
:
val x = leftPadding + i * (timeSpanPerTick / timeSpan) * viewWidth (x, 0f, x, headerHeight, axisPaint) (formatTime(minTime + i * timeSpanPerTick), x, textY, textPaint)
- Below, draw the rectangular bars and the task name of each task row in turn.
-
Status management and refresh
when
scaleFactor
Or when the task list is updated, call?.notifyDataSetChanged()
;Available
DiffUtil
Fine refresh;
4. Integrate code
The following integrates all core source files and layout files into the same code block, distinguishes files with comments, and attaches detailed comments.
// --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------/* plugins { id '' id 'kotlin-android' id 'kotlin-kapt' } android { compileSdkVersion 34 defaultConfig { applicationId "" minSdkVersion 21 targetSdkVersion 34 versionCode 1 versionName "1.0" } buildFeatures { viewBinding true } } dependencies { implementation ":core-ktx:1.10.1" implementation ":appcompat:1.7.0" implementation ":material:1.9.0" implementation ":constraintlayout:2.1.4" implementation ":recyclerview:1.3.1" implementation ":lifecycle-viewmodel-ktx:2.6.1" implementation ":lifecycle-livedata-ktx:2.6.1" implementation ":room-runtime:2.5.2" kapt ":room-compiler:2.5.2" implementation ":threetenbp:1.6.0" // or ThreeTenABP implementation ":threetenabp:1.4.4" } */ // ---------------- document: ----------------package import import import import /** * Task: Room entity, representing a task in the Gantt chart */ @Entity(tableName = "tasks") data class Task( @PrimaryKey(autoGenerate = true) val id: Long = 0, val name: String, val startTime: Long, // millisecond timestamp val endTime: Long, val color: Int // ARGB color) // ---------------- document: ----------------package import import .* /** * TaskDao: Task addition, deletion, modification and search interface */ @Dao interface TaskDao { @Query("SELECT * FROM tasks ORDER BY startTime") fun getAllTasks(): LiveData<List<Task>> @Insert(onConflict = ) suspend fun insert(task: Task) @Delete suspend fun delete(task: Task) } // ---------------- document: ----------------package import import /** * AppDatabase: Room database */ @Database(entities = [Task::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun taskDao(): TaskDao } // ---------------- document: ----------------package import import .* import import import import /** * GanttViewModel: Holds a task list, provides addition, deletion, modification and check */ class GanttViewModel(application: Application) : AndroidViewModel(application) { private val db = (application, AppDatabase::, "").build() private val dao = () val tasks: LiveData<List<Task>> = () fun addTask(task: Task) = { (task) } fun deleteTask(task: Task) = { (task) } } // ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------<?xml version="1.0" encoding="utf-8"?> < xmlns:andro xmlns:app="/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- List of task names on the left --> < android: android:layout_width="120dp" android:layout_height="0dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"/> <!-- Gantt chart area on the right,Roll horizontally --> <HorizontalScrollView android: 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"> < android: android:layout_width="wrap_content" android:layout_height="match_parent"/> </HorizontalScrollView> <!-- Add task button --> < android: 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"/> </> // --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:andro android: android:layout_width="match_parent" android:layout_height="48dp" android:gravity="center_vertical" android:paddingStart="8dp" android:textSize="16sp" android:textColor="#333"/> // ---------------- document: ----------------package import import import import import import import /** * TaskNameAdapter: List of task names on the left */ class TaskNameAdapter : ListAdapter<Task, >(DIFF) { companion object { val DIFF = object : <Task>() { override fun areItemsTheSame(old: Task, new: Task) = == override fun areContentsTheSame(old: Task, new: Task) = old == new } } inner class NameVH(val binding: ItemTaskNameBinding) : () override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = NameVH(((), parent, false)) override fun onBindViewHolder(holder: NameVH, position: Int) { = getItem(position).name } } // ---------------- document: ----------------package import import .* import import .* import import import import import import /** * GanttChartView: Custom Gantt Chart View */ class GanttChartView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ): View(context, attrs), , { // data private var tasks: List<Task> = emptyList() private var minTime = Long.MAX_VALUE private var maxTime = 0L private var timeSpan = 1L // ms // Paintbrush private val barPaint = Paint(Paint.ANTI_ALIAS_FLAG) private val axisPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = ; strokeWidth=2f } private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = ; textSize = 24f } // Layout parameters private val rowHeight = 80f private val headerHeight = 80f private var scaleFactor = 1f private var offsetX = 0f // Gestures private val scroller = OverScroller(context) private val gestureDetector = GestureDetector(context, this) private val scaleDetector = ScaleGestureDetector(context, this) init { = } /** Set tasks externally and recalculate the range */ fun setTasks(list: List<Task>) { tasks = list if (()) { minTime = { } maxTime = { } timeSpan = maxTime - minTime } invalidate() } override fun onDraw(canvas: Canvas) { (canvas) if (()) return // 1. Draw the timeline val totalWidth = () * scaleFactor val tickCount = 6 for (i in 0..tickCount) { val x = offsetX + i / () * totalWidth (x, 0f, x, headerHeight, axisPaint) val time = minTime + i / () * timeSpan val label = (time) .atZone(()).toLocalDate().toString() (label, x + 10, headerHeight - 20, textPaint) } // 2. Draw each row of task bars { idx, task -> val top = headerHeight + idx * rowHeight val bottom = top + rowHeight * 0.6f // Calculate left and right val left = offsetX + ( - minTime) / () * totalWidth val right = offsetX + ( - minTime) / () * totalWidth = (left, top + 10, right, bottom, barPaint) } } // ================== Gestures and zoom ======================= override fun onTouchEvent(event: MotionEvent): Boolean { (event) if (!) { (event) } return true } override fun onScale(detector: ScaleGestureDetector): Boolean { scaleFactor *= scaleFactor = (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 = (-(), () * scaleFactor) invalidate() return true } override fun onLongPress(e: MotionEvent) {} override fun onFling(e1: MotionEvent, e2: MotionEvent, vx: Float, vy: Float): Boolean { ( (), 0, (), 0, (-width).toInt(), (width * scaleFactor).toInt(), 0, 0 ) postInvalidateOnAnimation() return true } override fun computeScroll() { if (()) { offsetX = () invalidate() } } } // ---------------- document: ----------------package import import import import import import import import /** * MainActivity: Sample Gantt Chart Display */ class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private val vm: GanttViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { (savedInstanceState) binding = (layoutInflater) setContentView() // List of task names on the left val nameAdapter = TaskNameAdapter() { layoutManager = LinearLayoutManager(this@MainActivity) adapter = nameAdapter } // Observe task data (this) { list -> (list) (list) } // Added sample tasks { val now = () val task = Task( name = "Task${now%100}", startTime = now, endTime = now + 3600_000 * (1 + (now%5).toInt()), color = (((now/1000)%255).toInt(),120,150) ) (task) } } } // ---------------- document: activity_main.xml (ViewBinding) ----------------<?xml version="1.0" encoding="utf-8"?> <layout xmlns:andro xmlns:app="/apk/res-auto"> <data/> < android:layout_width="match_parent" android:layout_height="match_parent"> < android: 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: 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"> < android: android:layout_width="2000dp" android:layout_height="match_parent"/> </HorizontalScrollView> < android: 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"/> </> </layout>
V. Method description
(): Get the task list asynchronously and use
LiveData
Form exposure, automatically monitor data changes;()/deleteTask():exist
ViewModelScope
Execute Room operations in to ensure that the UI thread is not blocked;TaskNameAdapter: The vertical list on the left, only responsible for displaying the task name;
(list): Accept the task list, calculate
minTime
、maxTime
、timeSpan
and refresh the view;(): First draw the top time scale, then traverse the task list to draw each task bar;
and: Respond to single-finger scrolling to pan the view and double-finger zoom to adjust
scaleFactor
;OverScroller:exist
onFling()
Initiate inertial sliding and incomputeScroll()
Continuous updateoffsetX
;-
MainActivity:
Bind
RecyclerView
andGanttChartView
;observe
, submit data in both directions;
Click
fabAdd
Randomly added new task demonstration effect.
6. Project Summary
6.1 Results Review
Completed a native Android Gantt Chart component, supporting vertical task lists, horizontal timelines, automatic calculation of coordinates and adaptive scaling;
Adopt
RecyclerView
and PureView
Combining drawing to achieve high-performance rendering and interaction;Supports gesture scaling, scrolling and inertial sliding, making the user experience smooth;
The data layer is based on Room + LiveData + ViewModel, which realizes offline storage and real-time refresh.
6.2 Technology gains
A deep understanding of Canvas coordinate mapping, time → pixel conversion and custom View drawing mechanism;
Mastered
GestureDetector
、ScaleGestureDetector
、OverScroller
Equal gestures and inertial sliding API;Learn to integrate Room database and UI components in the MVVM architecture;
Learned how to implement configurable, high-performance large data visualization components on Android.
6.3 Follow-up optimization
Dynamic loading: For tasks with large time spans, load time scales and task bars as needed to avoid drawing too many elements at once;
Task interaction: Add task drag to change the start and end time, slide to adjust the duration, and press to pop up the context menu;
View linkage: The task list is linked to the Gantt chart, click on the task name to highlight the Gantt bar, click on the Gantt bar to scroll the list;
Theme and style: Support dark mode, customizable line height, bar height, scale font, interval color, etc.;
Performance detection: Use Systrace to analyze drawing and gesture responses to further optimize frame rates;
Accessible: Added to Gantt bars and time scales
contentDescription
, improve the A11Y experience;Unit testing and UI automation testing: Focus on testing time mapping, scaling logic and sliding boundaries.
The above is the detailed explanation of the code for implementing the effect of work management Gantt charts based on Android. For more information about Android's work management Gantt charts, please pay attention to my other related articles!
Related Articles
Android application development uses graphic and text usage methods for calling c++ jni through java
This article mainly introduces2013-11-11Android programming implements the masking, capturing and modifying methods of Home keys
This article mainly introduces Android programming to implement the masking, capture and modifying methods of Home keys. It analyzes the relevant skills of using onAttachedToWindow to capture Home keys. Friends who need it can refer to it.2016-06-06Detailed explanation of jump, animation and transmission of Flutter routes (easiest)
This article mainly introduces you to the relevant information about the jump, animation and parameter transmission of Flutter routing. The example code is introduced in this article in detail, which has certain reference learning value for everyone's study or work. Friends who need it, please learn with the editor below.2019-01-01Android implements top navigation indicators that imitate NetEase News
This article mainly introduces the relevant information about Android's top navigation indicator imitating NetEase News. It is very good and has reference value. Friends who need it can refer to it.2016-08-08Android advanced animation chapter SVG vector animation example
Vector animation is an animation that uses mathematical equations to describe complex curves on the screen in a computer, and uses abstract motion characteristics of graphics to record changing picture information. This article will take you to understand vector animation in Android.2021-11-11Overview of the Android App's operating environment and Android system architecture
This article mainly introduces the Android App's operating environment and Android system architecture overview, and introduces knowledge points such as the application process isolation mechanism. Friends who need it can refer to it2016-03-03Android programming method to realize the sliding effect of novel readers
This article mainly introduces the method of Android programming to realize the sliding effect of novel readers, and involves relevant implementation techniques for the sliding effect of onTouch event. It has certain reference value. Friends who need it can refer to it.2015-10-10Source code details the usage of () in Android
This article uses the source code to analyze the usage of () in Android, the common problems and solutions, and learn them together.2017-12-12Solve the problem that Android virtual keys obscures the page content
Today, the editor will share with you an article to solve the problem that Android virtual buttons obscures the content of the page, which is of great reference value. I hope it will be helpful to everyone. Let's take a look with the editor2018-07-07Android platform HttpGet and HttpPost request instance
The Android platform, which comes from the Internet search engine giant, needs no need to say more about its support for the Internet. Apache's HttpClient module has been integrated into the Android SDK. Using the HttpClient module, we can use the HTTP protocol to connect to the network2014-05-05