Skip to content

Commit

Permalink
Merge pull request #9 from lriccardo/dev
Browse files Browse the repository at this point in the history
Release 1.1.0
  • Loading branch information
lriccardo committed Jan 9, 2022
2 parents a4d511e + 6480b4b commit 3b0f55d
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 68 deletions.
35 changes: 30 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# TimelineView
[![](https://jitpack.io/v/lriccardo/TimelineView.svg)](https://jitpack.io/#lriccardo/TimelineView)
[![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21)

[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-TimelineView-brightgreen.svg?style=flat)](https://android-arsenal.com/details/1/8268)
[![AndroidWeekly](https://androidweekly.net/issues/issue-498/badge)](https://androidweekly.net/issues/issue-498)

A customizable and easy-to-use Timeline View library for Android

Expand All @@ -12,13 +16,13 @@ Can be used as a standalone view or as a RecyclerView decorator
<table>
<th>Automatically adapts to the item height and supports expand animations</th>
<th>View type</th>
<th>Preview</th>
<th>Preview</th>
<th>View type</th>
<th>Preview</th>
<th>Preview</th>
<th>View type</th>
<th>Preview</th>
<tr>
<td align="center" rowspan="2"><img src="https://github.com/lriccardo/TimelineView/raw/main/screens/expand.gif" alt="expand" width="250"/></td>
<td align="center" rowspan="2"><img src="https://github.com/lriccardo/TimelineView/raw/main/screens/demo.gif" alt="demo" width="250"/></td>
<td>first</td>
<td><img src="https://github.com/lriccardo/TimelineView/raw/main/screens/first.jpg" alt="first" width="200"/></td>
<td>middle</td>
Expand Down Expand Up @@ -52,7 +56,7 @@ allprojects {

```gradle
dependencies {
implementation 'com.github.lriccardo:TimelineView:1.0.5'
implementation 'com.github.lriccardo:TimelineView:1.1.0'
}
```

Expand All @@ -74,6 +78,10 @@ recyclerView.addItemDecoration(

- Customization

You can provide a custom drawable for the indicators using `indicatorDrawable` or `indicatorDrawableRes` (`indicatorDrawable` overrides `indicatorDrawableRes`), if both are `null` a circle will be drawn (using the other customization parameters).

If you pass a drawable reference (`indicatorDrawableRes`), `ContextCompat.getDrawable()` will be used internally.

<table>
<th>Field</th>
<th>Accepted values</th>
Expand All @@ -83,6 +91,16 @@ recyclerView.addItemDecoration(
<td>IndicatorStyle (Filled | Empty | Checked)</td>
<td>Filled</td>
</tr>
<tr>
<td>indicatorDrawable</td>
<td>Drawable</td>
<td>null</td>
</tr>
<tr>
<td>indicatorDrawableRes</td>
<td>@DrawableRes Int</td>
<td>null</td>
</tr>
<tr>
<td>indicatorSize</td>
<td>Float</td>
Expand Down Expand Up @@ -154,10 +172,12 @@ recyclerView.addItemDecoration(
- Advanced customization

If your `RecyclerView.Adapter` implements `TimelineAdapter` you can customize how each item of your list is drawn.
Implementing one or all of these methods, allows you to use the `position` argument to return a different customization for some of your items.
Implementing one or more of these methods, allows you to use the `position` argument to return a different customization for some of your items.
```kotlin
interface TimelineAdapter {
fun getTimelineViewType(position: Int): TimelineView.ViewType?
fun getIndicatorDrawable(position: Int): Drawable?
@DrawableRes fun getIndicatorDrawableRes(position: Int): Int?
fun getIndicatorStyle(position: Int): TimelineView.IndicatorStyle?
fun getIndicatorColor(position: Int): Int?
fun getLineColor(position: Int): Int?
Expand Down Expand Up @@ -193,6 +213,11 @@ recyclerView.addItemDecoration(
<td>filled | empty | checked</td>
<td>filled</td>
</tr>
<tr>
<td>app:indicator_drawable</td>
<td>Drawable</td>
<td>null</td>
</tr>
<tr>
<td>app:indicator_size</td>
<td>Dimension</td>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.lriccardo.timelineview

import android.graphics.drawable.Drawable
import androidx.annotation.DrawableRes

interface TimelineAdapter {
fun getTimelineViewType(position: Int): TimelineView.ViewType? = null
fun getIndicatorDrawable(position: Int): Drawable? = null
@DrawableRes fun getIndicatorDrawableRes(position: Int): Int? = null
fun getIndicatorStyle(position: Int): TimelineView.IndicatorStyle? = null
fun getIndicatorColor(position: Int): Int? = null
fun getLineColor(position: Int): Int? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ package com.lriccardo.timelineview

import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView

class TimelineDecorator(
val indicatorStyle: TimelineView.IndicatorStyle = TimelineView.IndicatorStyle.Filled,
val indicatorDrawable: Drawable? = null,
@DrawableRes val indicatorDrawableRes: Int? = null,
val indicatorSize: Float = 12.toPx().toFloat(),
val indicatorYPosition: Float = 0.5f,
val checkedIndicatorSize: Float? = null,
Expand Down Expand Up @@ -64,6 +69,12 @@ class TimelineDecorator(
timelineView.viewType = it
} ?: timelineView.setType(itemPosition, parent.adapter?.itemCount ?: -1)

(getIndicatorDrawable(itemPosition) ?: indicatorDrawable)?.let {
timelineView.indicatorDrawable = it
} ?: (getIndicatorDrawableRes(itemPosition) ?: indicatorDrawableRes)?.let {
timelineView.indicatorDrawable = ContextCompat.getDrawable(parent.context, it)
}

(getIndicatorColor(itemPosition) ?: indicatorColor)?.let {
timelineView.indicatorColor = it
}
Expand Down
146 changes: 83 additions & 63 deletions TimelineView/src/main/java/com/lriccardo/timelineview/TimelineView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.lriccardo.timelineview
import android.content.Context
import android.content.res.Resources
import android.graphics.*
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
Expand Down Expand Up @@ -35,7 +36,10 @@ class TimelineView @JvmOverloads constructor(

var viewType = ViewType.FIRST

var indicatorDrawable: Drawable?

var indicatorSize: Float = 12.toPx().toFloat()

@ColorInt
var indicatorColor: Int = Color.RED
set(value) {
Expand Down Expand Up @@ -111,7 +115,12 @@ class TimelineView @JvmOverloads constructor(
).apply {
try {
viewType =
ViewType.values()[getInteger(R.styleable.TimelineView_timeline_item_type, viewType.ordinal)]
ViewType.values()[getInteger(
R.styleable.TimelineView_timeline_item_type,
viewType.ordinal
)]

indicatorDrawable = getDrawable(R.styleable.TimelineView_indicator_drawable)

indicatorSize = getDimensionPixelSize(
R.styleable.TimelineView_indicator_size,
Expand Down Expand Up @@ -209,6 +218,7 @@ class TimelineView @JvmOverloads constructor(
strokeWidth = lineWidth
}
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val size = when(indicatorStyle){
Expand All @@ -225,96 +235,106 @@ class TimelineView @JvmOverloads constructor(
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)

val lineX = (width / 2).toFloat()

var topLineYStart: Float
var topLineYEnd: Float

var bottomLineYStart: Float
var bottomLineYEnd: Float

val indicatorCenterX = (width / 2).toFloat()
val indicatorCenterY =
(height * indicatorYPosition).coerceIn(indicatorSize, height - indicatorSize)

var drawIndicator = true
var drawTopLine = false
var drawBottomLine = false
var lineX = (width / 2).toFloat()

when (viewType) {
ViewType.FIRST -> {
drawTopLine = false
drawBottomLine = true
drawIndicator = true
}
ViewType.MIDDLE -> {
drawTopLine = true
drawBottomLine = true
drawIndicator = true
}
ViewType.LAST -> {
drawTopLine = true
drawBottomLine = false
drawIndicator = true
}
ViewType.SPACER -> {
drawTopLine = true
drawBottomLine = true
drawIndicator = false
}
}
var topLineYStart = 0f
var topLineYEnd: Float = indicatorCenterY

topLineYStart = 0f
val bottomLineYStart = height.toFloat()
var bottomLineYEnd = indicatorCenterY

// The top line should start with a gap, so we start drawing below 'lineDashGap'
if (lineStyle == LineStyle.Dashed && (indicatorCenterY - indicatorSize) > lineDashGap)
topLineYStart += lineDashGap

val drawIndicator = viewType != ViewType.SPACER
val drawTopLine = viewType != ViewType.FIRST
val drawBottomLine = viewType != ViewType.LAST

if (drawIndicator) {
if (indicatorDrawable != null) {
drawDrawableIndicator(indicatorCenterX, indicatorCenterY, canvas)

indicatorDrawable?.bounds?.let {
topLineYEnd =
(it.top - linePadding).coerceAtLeast(topLineYStart)
bottomLineYEnd =
(it.bottom + linePadding).coerceAtMost(bottomLineYStart)
lineX = it.centerX().toFloat()
}
} else {
drawCircleIndicator(canvas, indicatorCenterX, indicatorCenterY)

bottomLineYStart = height.toFloat()
if(drawIndicator) {
topLineYEnd =
(indicatorCenterY - indicatorSize - linePadding).coerceAtLeast(topLineYStart)
bottomLineYEnd =
(indicatorCenterY + indicatorSize + linePadding).coerceAtMost(bottomLineYStart)
topLineYEnd =
(indicatorCenterY - indicatorSize - linePadding).coerceAtLeast(topLineYStart)
bottomLineYEnd =
(indicatorCenterY + indicatorSize + linePadding).coerceAtMost(bottomLineYStart)
}
} else {
topLineYEnd = indicatorCenterY
bottomLineYEnd = indicatorCenterY
}

if(drawTopLine) {
if (drawTopLine) {
canvas.drawLine(lineX, topLineYStart, lineX, topLineYEnd, linePaint)
}
if(drawBottomLine){

if (drawBottomLine) {
canvas.drawLine(lineX, bottomLineYStart, lineX, bottomLineYEnd, linePaint)
}
}

if (drawIndicator) {
if (indicatorStyle == IndicatorStyle.Checked) {
private fun drawCircleIndicator(
canvas: Canvas,
indicatorCenterX: Float,
indicatorCenterY: Float
) {
if (indicatorStyle == IndicatorStyle.Checked) {
// Outer circle
canvas.drawCircle(
indicatorCenterX,
indicatorCenterY,
indicatorSize,
indicatorPaint
)

// Inner circle
checkedIndicatorPaint?.let {
it.color = indicatorColor
canvas.drawCircle(
indicatorCenterX,
indicatorCenterY,
indicatorSize,
indicatorPaint
)
checkedIndicatorPaint?.let {
it.color = indicatorColor
canvas.drawCircle(
indicatorCenterX,
indicatorCenterY,
checkedIndicatorSize,
it
)
}
} else {
canvas.drawCircle(
indicatorCenterX,
indicatorCenterY,
indicatorSize,
indicatorPaint
checkedIndicatorSize,
it
)
}
} else {
canvas.drawCircle(
indicatorCenterX,
indicatorCenterY,
indicatorSize,
indicatorPaint
)
}
}

private fun drawDrawableIndicator(
indicatorCenterX: Float,
indicatorCenterY: Float,
canvas: Canvas
) {
val left = (indicatorCenterX - indicatorSize).toInt()
val top = (indicatorCenterY - indicatorSize).toInt()
val right = (indicatorCenterX + indicatorSize).toInt()
val bottom = (indicatorCenterY + indicatorSize).toInt()
indicatorDrawable?.setBounds(left, top, right, bottom)
indicatorDrawable?.draw(canvas)
}

fun setType(position: Int, totalItems: Int) {
when (position) {
0 -> viewType = ViewType.FIRST
Expand Down
1 change: 1 addition & 0 deletions TimelineView/src/main/res/values/attrs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<enum name="empty" value="1"/>
<enum name="checked" value="2"/>
</attr>
<attr name="indicator_drawable" format="reference"/>
<attr name="indicator_size" format="dimension"/>
<attr name="indicator_color" format="color"/>
<attr name="indicator_y_position" format="float"/>
Expand Down
File renamed without changes
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="8dp" />
<solid android:color="#ffffff" />
</shape>

0 comments on commit 3b0f55d

Please sign in to comment.