timer UI improvements + bug fixes

- add page indicator
- add default timer based on saved prefs when the database is created
- post scolling to the first timer when a new timer is added
This commit is contained in:
Paul Akhamiogu
2021-09-02 23:47:40 +01:00
parent 9df105ae8d
commit 037eac6864
8 changed files with 763 additions and 26 deletions

View File

@ -5,9 +5,12 @@ import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import androidx.sqlite.db.SupportSQLiteDatabase
import com.simplemobiletools.clock.extensions.config
import com.simplemobiletools.clock.helpers.Converters import com.simplemobiletools.clock.helpers.Converters
import com.simplemobiletools.clock.interfaces.TimerDao import com.simplemobiletools.clock.interfaces.TimerDao
import com.simplemobiletools.clock.models.Timer import com.simplemobiletools.clock.models.Timer
import java.util.concurrent.Executors
@Database(entities = [Timer::class], version = 1) @Database(entities = [Timer::class], version = 1)
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
@ -23,6 +26,12 @@ abstract class AppDatabase : RoomDatabase() {
synchronized(AppDatabase::class) { synchronized(AppDatabase::class) {
if (db == null) { if (db == null) {
db = Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app.db") db = Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app.db")
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
insertDefaultTimer(context)
}
})
.build() .build()
db!!.openHelper.setWriteAheadLoggingEnabled(true) db!!.openHelper.setWriteAheadLoggingEnabled(true)
} }
@ -31,6 +40,25 @@ abstract class AppDatabase : RoomDatabase() {
return db!! return db!!
} }
private fun insertDefaultTimer(context: Context) {
Executors.newSingleThreadScheduledExecutor().execute {
val config = context.config
db!!.TimerDao().insertOrUpdateTimer(
Timer(
id = null,
seconds = config.timerSeconds,
state = config.timerState,
vibrate = config.timerVibrate,
soundUri = config.timerSoundUri,
soundTitle = config.timerSoundTitle,
label = config.timerLabel ?: "",
createdAt = System.currentTimeMillis(),
channelId = config.timerChannelId,
)
)
}
}
fun destroyInstance() { fun destroyInstance() {
db = null db = null
} }

View File

@ -53,10 +53,20 @@ class TimerFragment : Fragment() {
timer_view_pager.setPageTransformer { _, _ -> } timer_view_pager.setPageTransformer { _, _ -> }
timer_view_pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { timer_view_pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
Log.i(TAG, "onPageSelected: $position")
updateViews(position) updateViews(position)
indicator_view.setCurrentPosition(0)
} }
}) })
activity?.let {
val textColor = it.config.textColor
indicator_view.setSelectedDotColor(textColor)
indicator_view.setDotColor(textColor.adjustAlpha(0.5f))
indicator_view.attachToPager(timer_view_pager)
}
timer_add.setOnClickListener { timer_add.setOnClickListener {
activity?.hideKeyboard(it) activity?.hideKeyboard(it)
activity?.timerHelper?.insertNewTimer { activity?.timerHelper?.insertNewTimer {
@ -101,14 +111,12 @@ class TimerFragment : Fragment() {
private fun updateViews(position: Int) { private fun updateViews(position: Int) {
activity?.runOnUiThread { activity?.runOnUiThread {
if (timerAdapter.itemCount > 0) { if (timerAdapter.itemCount > position) {
val timer = timerAdapter.getItemAt(position) val timer = timerAdapter.getItemAt(position)
updateViewStates(timer.state) updateViewStates(timer.state)
view.timer_play_pause.beVisible() view.timer_play_pause.beVisible()
} else { } else {
view.timer_delete.beGone() Log.e(TAG, "updateViews: position $position is greater than adapter itemCount ${timerAdapter.itemCount}")
view.timer_play_pause.beGone()
view.timer_reset.beGone()
} }
} }
} }
@ -117,15 +125,18 @@ class TimerFragment : Fragment() {
activity?.timerHelper?.getTimers { timers -> activity?.timerHelper?.getTimers { timers ->
Log.d(TAG, "refreshTimers: $timers") Log.d(TAG, "refreshTimers: $timers")
timerAdapter.submitList(timers) { timerAdapter.submitList(timers) {
Log.e(TAG, "submitted list: timerPositionToScrollTo=$timerPositionToScrollTo") view.timer_view_pager.post {
if (timerPositionToScrollTo != INVALID_POSITION && timerAdapter.itemCount > timerPositionToScrollTo) { Log.e(TAG, "submitted list: timerPositionToScrollTo=$timerPositionToScrollTo")
Log.e(TAG, "scrolling to position=$timerPositionToScrollTo") if (timerPositionToScrollTo != INVALID_POSITION && timerAdapter.itemCount > timerPositionToScrollTo) {
view.timer_view_pager.setCurrentItem(timerPositionToScrollTo, false) Log.e(TAG, "scrolling to position=$timerPositionToScrollTo")
timerPositionToScrollTo = INVALID_POSITION view.timer_view_pager.setCurrentItem(timerPositionToScrollTo, false)
} else if (scrollToLatest) { timerPositionToScrollTo = INVALID_POSITION
view.timer_view_pager.setCurrentItem(0, false) } else if (scrollToLatest) {
Log.e(TAG, "scrolling to latest")
view.timer_view_pager.setCurrentItem(0, false)
}
updateViews(timer_view_pager.currentItem)
} }
updateViews(timer_view_pager.currentItem)
} }
} }
} }

View File

@ -1,13 +1,10 @@
package com.simplemobiletools.clock.helpers package com.simplemobiletools.clock.helpers
import android.content.Context import android.content.Context
import android.media.RingtoneManager
import android.util.Log import android.util.Log
import com.simplemobiletools.clock.extensions.config
import com.simplemobiletools.clock.extensions.timerDb import com.simplemobiletools.clock.extensions.timerDb
import com.simplemobiletools.clock.models.Timer import com.simplemobiletools.clock.models.Timer
import com.simplemobiletools.clock.models.TimerState
import com.simplemobiletools.commons.extensions.getDefaultAlarmSound
import com.simplemobiletools.commons.extensions.getDefaultAlarmTitle
import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.commons.helpers.ensureBackgroundThread
class TimerHelper(val context: Context) { class TimerHelper(val context: Context) {
@ -42,17 +39,18 @@ class TimerHelper(val context: Context) {
fun insertNewTimer(callback: () -> Unit = {}) { fun insertNewTimer(callback: () -> Unit = {}) {
ensureBackgroundThread { ensureBackgroundThread {
val config = context.config
timerDao.insertOrUpdateTimer( timerDao.insertOrUpdateTimer(
Timer( Timer(
id = null, id = null,
// seconds = DEFAULT_TIME, seconds = config.timerSeconds,
seconds = 5, state = config.timerState,
TimerState.Idle, vibrate = config.timerVibrate,
false, soundUri = config.timerSoundUri,
context.getDefaultAlarmSound(RingtoneManager.TYPE_ALARM).uri, soundTitle = config.timerSoundTitle,
context.getDefaultAlarmTitle(RingtoneManager.TYPE_ALARM), label = config.timerLabel ?: "",
"", createdAt = System.currentTimeMillis(),
System.currentTimeMillis(), channelId = config.timerChannelId,
) )
) )

View File

@ -0,0 +1,577 @@
package com.simplemobiletools.clock.views.pageindicator
import android.animation.ArgbEvaluator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.util.SparseArray
import android.view.View
import android.widget.LinearLayout
import androidx.annotation.ColorInt
import androidx.annotation.IntDef
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.simplemobiletools.clock.R
/**
* Page indicator that scrolls to selected item based on
* [PagerIndicator](https://github.com/TinkoffCreditSystems/PagerIndicator)
* */
class PagerIndicator @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.scrollingPagerIndicatorStyle
) : View(context, attrs, defStyleAttr) {
@IntDef(RecyclerView.HORIZONTAL, RecyclerView.VERTICAL)
annotation class Orientation
private var infiniteDotCount = 0
private val dotMinimumSize: Int
private val dotNormalSize: Int
private val dotSelectedSize: Int
private val spaceBetweenDotCenters: Int
private var visibleDotCount = 0
private var visibleDotThreshold: Int
private var orientation: Int
private var visibleFramePosition = 0f
private var visibleFrameWidth = 0f
private var firstDotOffset = 0f
private var dotScale: SparseArray<Float>? = null
private var itemCount = 0
private val paint: Paint
private val colorEvaluator = ArgbEvaluator()
@ColorInt
private var dotColor: Int
@ColorInt
private var selectedDotColor: Int
private var looped: Boolean
private var attachRunnable: Runnable? = null
private var currentAttacher: PagerAttacher<*>? = null
private var dotCountInitialized = false
/**
* Sets dot count
*
* @param count new dot count
*/
var dotCount: Int
get() = if (looped && itemCount > visibleDotCount) {
infiniteDotCount
} else {
itemCount
}
set(count) {
initDots(count)
}
init {
val attributes = context.obtainStyledAttributes(
attrs,
R.styleable.PagerIndicator,
defStyleAttr,
R.style.PagerIndicator)
dotColor = attributes.getColor(R.styleable.PagerIndicator_spi_dotColor, 0)
selectedDotColor =
attributes.getColor(R.styleable.PagerIndicator_spi_dotSelectedColor, dotColor)
dotNormalSize =
attributes.getDimensionPixelSize(R.styleable.PagerIndicator_spi_dotSize, 0)
dotSelectedSize =
attributes.getDimensionPixelSize(R.styleable.PagerIndicator_spi_dotSelectedSize,
0)
val dotMinimumSize =
attributes.getDimensionPixelSize(R.styleable.PagerIndicator_spi_dotMinimumSize,
-1)
this.dotMinimumSize = if (dotMinimumSize <= dotNormalSize) dotMinimumSize else -1
spaceBetweenDotCenters =
attributes.getDimensionPixelSize(R.styleable.PagerIndicator_spi_dotSpacing,
0) + dotNormalSize
looped = attributes.getBoolean(R.styleable.PagerIndicator_spi_looped, false)
val visibleDotCount =
attributes.getInt(R.styleable.PagerIndicator_spi_visibleDotCount, 5)
setVisibleDotCount(visibleDotCount)
visibleDotThreshold =
attributes.getInt(R.styleable.PagerIndicator_spi_visibleDotThreshold, 2)
orientation = attributes.getInt(R.styleable.PagerIndicator_spi_orientation,
RecyclerView.HORIZONTAL)
attributes.recycle()
paint = Paint()
paint.isAntiAlias = true
if (isInEditMode) {
dotCount = visibleDotCount
onPageScrolled(visibleDotCount / 2, 0f)
}
}
/**
* You should make indicator looped in your PagerAttacher implementation if your custom pager is looped too
* If pager has less items than visible_dot_count, indicator will work as usual;
* otherwise it will always be in infinite state.
*
* @param looped true if pager is looped
*/
fun setLooped(looped: Boolean) {
this.looped = looped
reattach()
invalidate()
}
/**
* @return not selected dot color
*/
@ColorInt
fun getDotColor(): Int {
return dotColor
}
/**
* Sets dot color
*
* @param color dot color
*/
fun setDotColor(@ColorInt color: Int) {
dotColor = color
invalidate()
}
/**
* @return the selected dot color
*/
@ColorInt
fun getSelectedDotColor(): Int {
return selectedDotColor
}
/**
* Sets selected dot color
*
* @param color selected dot color
*/
fun setSelectedDotColor(@ColorInt color: Int) {
selectedDotColor = color
invalidate()
}
/**
* Maximum number of dots which will be visible at the same time.
* If pager has more pages than visible_dot_count, indicator will scroll to show extra dots.
* Must be odd number.
*
* @return visible dot count
*/
fun getVisibleDotCount(): Int {
return visibleDotCount
}
/**
* Sets visible dot count. Maximum number of dots which will be visible at the same time.
* If pager has more pages than visible_dot_count, indicator will scroll to show extra dots.
* Must be odd number.
*
* @param visibleDotCount visible dot count
* @throws IllegalStateException when pager is already attached
*/
fun setVisibleDotCount(visibleDotCount: Int) {
require(visibleDotCount % 2 != 0) { "visibleDotCount must be odd" }
this.visibleDotCount = visibleDotCount
infiniteDotCount = visibleDotCount + 2
if (attachRunnable != null) {
reattach()
} else {
requestLayout()
}
}
/**
* The minimum number of dots which should be visible.
* If pager has less pages than visibleDotThreshold, no dots will be shown.
*
* @return visible dot threshold.
*/
fun getVisibleDotThreshold(): Int {
return visibleDotThreshold
}
/**
* Sets the minimum number of dots which should be visible.
* If pager has less pages than visibleDotThreshold, no dots will be shown.
*
* @param visibleDotThreshold visible dot threshold.
*/
fun setVisibleDotThreshold(visibleDotThreshold: Int) {
this.visibleDotThreshold = visibleDotThreshold
if (attachRunnable != null) {
reattach()
} else {
requestLayout()
}
}
/**
* The visible orientation of the dots
*
* @return dot orientation (RecyclerView.HORIZONTAL, RecyclerView.VERTICAL)
*/
@Orientation
fun getOrientation(): Int {
return orientation
}
/**
* Set the dot orientation
*
* @param orientation dot orientation (RecyclerView.HORIZONTAL, RecyclerView.VERTICAL)
*/
fun setOrientation(@Orientation orientation: Int) {
this.orientation = orientation
if (attachRunnable != null) {
reattach()
} else {
requestLayout()
}
}
/**
* Attaches indicator to ViewPager2
*
* @param pager pager to attach
*/
fun attachToPager(pager: ViewPager2) {
attachToPager<ViewPager2>(pager, ViewPager2Attacher())
}
/**
* Attaches to any custom pager
*
* @param pager pager to attach
* @param attacher helper which should setup this indicator to work with custom pager
*/
fun <T> attachToPager(pager: T, attacher: PagerAttacher<T>) {
detachFromPager()
attacher.attachToPager(this, pager)
currentAttacher = attacher
attachRunnable = Runnable {
itemCount = -1
attachToPager(pager, attacher)
}
}
/**
* Detaches indicator from pager.
*/
fun detachFromPager() {
if (currentAttacher != null) {
currentAttacher!!.detachFromPager()
currentAttacher = null
attachRunnable = null
}
dotCountInitialized = false
}
/**
* Detaches indicator from pager and attaches it again.
* It may be useful for refreshing after adapter count change.
*/
fun reattach() {
if (attachRunnable != null) {
attachRunnable!!.run()
invalidate()
}
}
/**
* This method must be called from ViewPager.OnPageChangeListener.onPageScrolled or from some
* similar callback if you use custom PagerAttacher.
*
* @param page index of the first page currently being displayed
* Page position+1 will be visible if offset is nonzero
* @param offset Value from [0, 1) indicating the offset from the page at position
*/
fun onPageScrolled(page: Int, offset: Float) {
require(!(offset < 0 || offset > 1)) { "Offset must be [0, 1]" }
if (page < 0 || page != 0 && page >= itemCount) {
throw IndexOutOfBoundsException("page must be [0, adapter.getItemCount())")
}
if (!looped || itemCount <= visibleDotCount && itemCount > 1) {
dotScale!!.clear()
if (orientation == LinearLayout.HORIZONTAL) {
scaleDotByOffset(page, offset)
if (page < itemCount - 1) {
scaleDotByOffset(page + 1, 1 - offset)
} else if (itemCount > 1) {
scaleDotByOffset(0, 1 - offset)
}
} else { // Vertical orientation
scaleDotByOffset(page, offset)
}
invalidate()
}
if (orientation == LinearLayout.HORIZONTAL) {
adjustFramePosition(offset, page)
} else {
adjustFramePosition(offset, page - 1)
}
invalidate()
}
/**
* Sets currently selected position (according to your pager's adapter)
*
* @param position new current position
*/
fun setCurrentPosition(position: Int) {
if (position != 0 && (position < 0 || position >= itemCount)) {
throw IndexOutOfBoundsException("Position must be [0, adapter.getItemCount()]")
}
if (itemCount == 0) {
return
}
adjustFramePosition(0f, position)
updateScaleInIdleState(position)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// Width
val measuredWidth: Int
// Height
val measuredHeight: Int
if (orientation == LinearLayoutManager.HORIZONTAL) {
// We ignore widthMeasureSpec because width is based on visibleDotCount
measuredWidth = if (isInEditMode) {
// Maximum width with all dots visible
(visibleDotCount - 1) * spaceBetweenDotCenters + dotSelectedSize
} else {
if (itemCount >= visibleDotCount) visibleFrameWidth.toInt() else (itemCount - 1) * spaceBetweenDotCenters + dotSelectedSize
}
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
// Height
val desiredHeight = dotSelectedSize
measuredHeight = when (heightMode) {
MeasureSpec.EXACTLY -> heightSize
MeasureSpec.AT_MOST -> desiredHeight.coerceAtMost(heightSize)
MeasureSpec.UNSPECIFIED -> desiredHeight
else -> desiredHeight
}
} else {
measuredHeight = if (isInEditMode) {
(visibleDotCount - 1) * spaceBetweenDotCenters + dotSelectedSize
} else {
if (itemCount >= visibleDotCount) visibleFrameWidth.toInt() else (itemCount) * spaceBetweenDotCenters + dotSelectedSize
}
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
// Width
val desiredWidth = dotSelectedSize
measuredWidth = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize
MeasureSpec.AT_MOST -> desiredWidth.coerceAtMost(widthSize)
MeasureSpec.UNSPECIFIED -> desiredWidth
else -> desiredWidth
}
}
setMeasuredDimension(measuredWidth, measuredHeight)
}
override fun onDraw(canvas: Canvas) {
val dotCount = dotCount
if (dotCount < visibleDotThreshold) {
return
}
// Some empirical coefficients
val scaleDistance = (spaceBetweenDotCenters + (dotSelectedSize - dotNormalSize) / 2) * 0.7f
val smallScaleDistance = (dotSelectedSize / 2).toFloat()
val centerScaleDistance = 6f / 7f * spaceBetweenDotCenters
val firstVisibleDotPos =
(visibleFramePosition - firstDotOffset).toInt() / spaceBetweenDotCenters
var lastVisibleDotPos = (firstVisibleDotPos
+ (visibleFramePosition + visibleFrameWidth - getDotOffsetAt(firstVisibleDotPos)).toInt()
/ spaceBetweenDotCenters)
// If real dots count is less than we can draw inside visible frame, we move lastVisibleDotPos
// to the last item
if (firstVisibleDotPos == 0 && lastVisibleDotPos + 1 > dotCount) {
lastVisibleDotPos = dotCount - 1
}
for (i in firstVisibleDotPos..lastVisibleDotPos) {
val dot = getDotOffsetAt(i)
if (dot >= visibleFramePosition && dot < visibleFramePosition + visibleFrameWidth) {
var diameter: Float
var scale: Float
// Calculate scale according to current page position
scale = if (looped && itemCount > visibleDotCount) {
val frameCenter = visibleFramePosition + visibleFrameWidth / 2
if (dot >= frameCenter - centerScaleDistance
&& dot <= frameCenter
) {
(dot - frameCenter + centerScaleDistance) / centerScaleDistance
} else if (dot > frameCenter
&& dot < frameCenter + centerScaleDistance
) {
1 - (dot - frameCenter) / centerScaleDistance
} else {
0f
}
} else {
getDotScaleAt(i)
}
diameter = dotNormalSize + (dotSelectedSize - dotNormalSize) * scale
// Additional scale for dots at corners
if (itemCount > visibleDotCount) {
val currentScaleDistance = if (!looped && (i == 0 || i == dotCount - 1)) {
smallScaleDistance
} else {
scaleDistance
}
var size = width
if (orientation == LinearLayoutManager.VERTICAL) {
size = height
}
if (dot - visibleFramePosition < currentScaleDistance) {
val calculatedDiameter =
diameter * (dot - visibleFramePosition) / currentScaleDistance
if (calculatedDiameter <= dotMinimumSize) {
diameter = dotMinimumSize.toFloat()
} else if (calculatedDiameter < diameter) {
diameter = calculatedDiameter
}
} else if (dot - visibleFramePosition > size - currentScaleDistance) {
val calculatedDiameter =
diameter * (-dot + visibleFramePosition + size) / currentScaleDistance
if (calculatedDiameter <= dotMinimumSize) {
diameter = dotMinimumSize.toFloat()
} else if (calculatedDiameter < diameter) {
diameter = calculatedDiameter
}
}
}
paint.color = calculateDotColor(scale)
if (orientation == LinearLayoutManager.HORIZONTAL) {
canvas.drawCircle(dot - visibleFramePosition, (
measuredHeight / 2).toFloat(),
diameter / 2,
paint)
} else {
canvas.drawCircle((measuredWidth / 2).toFloat(),
dot - visibleFramePosition,
diameter / 2,
paint)
}
}
}
}
@ColorInt
private fun calculateDotColor(dotScale: Float): Int {
return colorEvaluator.evaluate(dotScale, dotColor, selectedDotColor) as Int
}
private fun updateScaleInIdleState(currentPos: Int) {
if (!looped || itemCount < visibleDotCount) {
dotScale!!.clear()
dotScale!!.put(currentPos, 1f)
invalidate()
}
}
private fun initDots(itemCount: Int) {
if (this.itemCount == itemCount && dotCountInitialized) {
return
}
this.itemCount = itemCount
dotCountInitialized = true
dotScale = SparseArray()
if (itemCount < visibleDotThreshold) {
requestLayout()
invalidate()
return
}
firstDotOffset =
if (looped && this.itemCount > visibleDotCount) 0f else dotSelectedSize / 2.toFloat()
visibleFrameWidth =
((visibleDotCount - 1) * spaceBetweenDotCenters + dotSelectedSize).toFloat()
requestLayout()
invalidate()
}
private fun adjustFramePosition(offset: Float, pos: Int) {
if (itemCount <= visibleDotCount) {
// Without scroll
visibleFramePosition = 0f
} else if (!looped && itemCount > visibleDotCount) {
// Not looped with scroll
val center = getDotOffsetAt(pos) + spaceBetweenDotCenters * offset
visibleFramePosition = center - visibleFrameWidth / 2
// Block frame offset near start and end
val firstCenteredDotIndex = visibleDotCount / 2
val lastCenteredDot = getDotOffsetAt(dotCount - 1 - firstCenteredDotIndex)
if (visibleFramePosition + visibleFrameWidth / 2 < getDotOffsetAt(firstCenteredDotIndex)) {
visibleFramePosition = getDotOffsetAt(firstCenteredDotIndex) - visibleFrameWidth / 2
} else if (visibleFramePosition + visibleFrameWidth / 2 > lastCenteredDot) {
visibleFramePosition = lastCenteredDot - visibleFrameWidth / 2
}
} else {
// Looped with scroll
val center = getDotOffsetAt(infiniteDotCount / 2) + spaceBetweenDotCenters * offset
visibleFramePosition = center - visibleFrameWidth / 2
}
}
private fun scaleDotByOffset(position: Int, offset: Float) {
if (dotScale == null || dotCount == 0) {
return
}
setDotScaleAt(position, 1 - Math.abs(offset))
}
private fun getDotOffsetAt(index: Int): Float {
return firstDotOffset + index * spaceBetweenDotCenters
}
private fun getDotScaleAt(index: Int): Float {
val scale = dotScale!![index]
return scale ?: 0f
}
private fun setDotScaleAt(index: Int, scale: Float) {
if (scale == 0f) {
dotScale!!.remove(index)
} else {
dotScale!!.put(index, scale)
}
}
/**
* Interface for attaching to custom pagers.
*
* @param <T> custom pager's class
</T> */
interface PagerAttacher<T> {
/**
* Here you should add all needed callbacks to track pager's item count, position and offset
* You must call:
* [PagerIndicator.setDotCount] - initially and after page selection,
* [PagerIndicator.setCurrentPosition] - initially and after page selection,
* [PagerIndicator.onPageScrolled] - in your pager callback to track scroll offset,
* [PagerIndicator.reattach] - each time your adapter items change.
*
* @param indicator indicator
* @param pager pager to attach
*/
fun attachToPager(indicator: PagerIndicator, pager: T)
/**
* Here you should unregister all callbacks previously added to pager and adapter
*/
fun detachFromPager()
}
}

View File

@ -0,0 +1,80 @@
package com.simplemobiletools.clock.views.pageindicator
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
/**
* ViewPager2 Attacher for [PagerIndicator]
) */
class ViewPager2Attacher : PagerIndicator.PagerAttacher<ViewPager2> {
private var dataSetObserver: AdapterDataObserver? = null
private var attachedAdapter: RecyclerView.Adapter<*>? = null
private var onPageChangeListener: OnPageChangeCallback? = null
private var pager: ViewPager2? = null
override fun attachToPager(indicator: PagerIndicator, pager: ViewPager2) {
attachedAdapter = pager.adapter
checkNotNull(attachedAdapter) { "Set adapter before call attachToPager() method" }
this.pager = pager
updateIndicatorDotsAndPosition(indicator)
dataSetObserver = object : AdapterDataObserver() {
override fun onChanged() {
indicator.reattach()
}
}
attachedAdapter!!.registerAdapterDataObserver(dataSetObserver!!)
onPageChangeListener = object : OnPageChangeCallback() {
var idleState = true
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixel: Int
) {
updateIndicatorOnPagerScrolled(indicator, position, positionOffset)
}
override fun onPageSelected(position: Int) {
if (idleState) {
updateIndicatorDotsAndPosition(indicator)
}
}
override fun onPageScrollStateChanged(state: Int) {
idleState = state == ViewPager2.SCROLL_STATE_IDLE
}
}
pager.registerOnPageChangeCallback(onPageChangeListener!!)
}
override fun detachFromPager() {
attachedAdapter!!.unregisterAdapterDataObserver(dataSetObserver!!)
pager!!.unregisterOnPageChangeCallback(onPageChangeListener!!)
}
private fun updateIndicatorDotsAndPosition(indicator: PagerIndicator) {
indicator.dotCount = attachedAdapter!!.itemCount
indicator.setCurrentPosition(pager!!.currentItem)
}
private fun updateIndicatorOnPagerScrolled(
indicator: PagerIndicator,
position: Int,
positionOffset: Float
) {
// ViewPager may emit negative positionOffset for very fast scrolling
val offset: Float = when {
positionOffset < 0 -> {
0f
}
positionOffset > 1 -> {
1f
}
else -> {
positionOffset
}
}
indicator.onPageScrolled(position, offset)
}
}

View File

@ -8,15 +8,27 @@
<androidx.viewpager2.widget.ViewPager2 <androidx.viewpager2.widget.ViewPager2
android:id="@+id/timer_view_pager" android:id="@+id/timer_view_pager"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginEnd="@dimen/medium_margin"
android:layout_marginBottom="@dimen/medium_margin" android:layout_marginBottom="@dimen/medium_margin"
android:orientation="vertical" android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@id/timer_play_pause" app:layout_constraintBottom_toTopOf="@id/timer_play_pause"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toStartOf="@id/indicator_view"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<com.simplemobiletools.clock.views.pageindicator.PagerIndicator
android:id="@+id/indicator_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/medium_margin"
app:layout_constraintBottom_toTopOf="@id/timer_play_pause"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.05"
app:spi_orientation="vertical" />
<ImageView <ImageView
android:id="@+id/timer_reset" android:id="@+id/timer_reset"
android:layout_width="@dimen/stopwatch_button_small_size" android:layout_width="@dimen/stopwatch_button_small_size"

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="scrollingPagerIndicatorStyle"/>
<declare-styleable name="PagerIndicator">
<attr name="spi_dotColor" format="color" />
<attr name="spi_dotSelectedColor" format="color" />
<attr name="spi_dotSize" format="dimension" />
<attr name="spi_dotSelectedSize" format="dimension" />
<attr name="spi_dotMinimumSize" format="dimension" />
<attr name="spi_dotSpacing" format="dimension" />
<attr name="spi_visibleDotCount" format="integer" />
<attr name="spi_visibleDotThreshold" format="integer" />
<attr name="spi_looped" format="boolean" />
<attr name="spi_orientation" format="enum">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
</declare-styleable>
</resources>

View File

@ -2,4 +2,14 @@
<style name="AppTheme" parent="AppTheme.Base"/> <style name="AppTheme" parent="AppTheme.Base"/>
<style name="PagerIndicator">
<item name="spi_dotColor">@android:color/darker_gray</item>
<item name="spi_dotSelectedColor">@android:color/darker_gray</item>
<item name="spi_dotSize">6dp</item>
<item name="spi_dotSelectedSize">10dp</item>
<item name="spi_dotSpacing">8dp</item>
<item name="spi_visibleDotCount">5</item>
<item name="spi_visibleDotThreshold">2</item>
<item name="spi_looped">false</item>
</style>
</resources> </resources>