瀏覽代碼

1.优化首页UI

王鹏鹏 2 年之前
父節點
當前提交
4e1732b574

+ 17 - 0
home/src/main/java/com/yingyangfly/home/widget/CharOrder.kt

@@ -0,0 +1,17 @@
+package com.yingyangfly.home.widget
+
+/**
+ * @author YvesCheung
+ * 2020/12/10
+ */
+object CharOrder {
+    const val Number = "0123456789"
+
+    const val Hex = "0123456789ABCDEF"
+
+    const val Binary = "01"
+
+    const val Alphabet = "abcdefghijklmnopqrstuvwxyz"
+
+    const val UpperAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+}

+ 42 - 0
home/src/main/java/com/yingyangfly/home/widget/CharOrderManager.kt

@@ -0,0 +1,42 @@
+package com.yingyangfly.home.widget
+
+import com.yingyangfly.home.widget.strategy.CharOrderStrategy
+import com.yingyangfly.home.widget.strategy.Direction
+import com.yingyangfly.home.widget.strategy.Strategy
+import java.util.*
+
+/**
+ * @author YvesCheung
+ * 2018/2/28
+ */
+internal class CharOrderManager {
+
+    var charStrategy: CharOrderStrategy = Strategy.NormalAnimation()
+
+    private val charOrderList = mutableListOf<LinkedHashSet<Char>>()
+
+    fun addCharOrder(orderList: Iterable<Char>) {
+        val list = mutableListOf(TextManager.EMPTY)
+        list.addAll(orderList)
+        val set = LinkedHashSet(list)
+        charOrderList.add(set)
+    }
+
+    fun findCharOrder(sourceText: CharSequence, targetText: CharSequence, index: Int)
+            : Pair<List<Char>, Direction> {
+        return charStrategy.findCharOrder(sourceText, targetText, index, charOrderList)
+    }
+
+    fun beforeCharOrder(sourceText: CharSequence, targetText: CharSequence) =
+        charStrategy.beforeCompute(sourceText, targetText, charOrderList)
+
+    fun afterCharOrder() = charStrategy.afterCompute()
+
+    fun getProgress(
+        previousProgress: PreviousProgress,
+        index: Int,
+        columns: List<List<Char>>,
+        charIndex: Int
+    ) =
+        charStrategy.nextProgress(previousProgress, index, columns, charIndex)
+}

+ 353 - 0
home/src/main/java/com/yingyangfly/home/widget/RollingTextView.kt

@@ -0,0 +1,353 @@
+@file:Suppress("unused")
+
+package com.yingyangfly.home.widget
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.content.Context
+import android.content.res.Resources
+import android.content.res.TypedArray
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Rect
+import android.graphics.Typeface
+import android.text.TextUtils
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.view.Gravity
+import android.view.View
+import android.view.animation.Interpolator
+import android.view.animation.LinearInterpolator
+import com.yingyang.home.R
+import com.yingyangfly.home.widget.strategy.CharOrderStrategy
+import com.yingyangfly.home.widget.strategy.Strategy
+
+/**
+ * @author YvesCheung
+ * 2018/2/26
+ */
+@Suppress("MemberVisibilityCanBePrivate")
+open class RollingTextView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0
+) : View(context, attrs, defStyleAttr) {
+
+    private var lastMeasuredDesiredWidth: Int = 0
+    private var lastMeasuredDesiredHeight: Int = 0
+
+    private val textPaint = Paint()
+    private val charOrderManager = CharOrderManager()
+    private val textManager = TextManager(textPaint, charOrderManager)
+
+    private var animator = ValueAnimator.ofFloat(1f)
+
+    private val viewBounds = Rect()
+    private var gravity: Int = Gravity.END
+    private var textStyle = Typeface.NORMAL
+
+    private var targetText: CharSequence = ""
+
+    var animationDuration: Long = 750L
+
+    var typeface: Typeface?
+        set(value) {
+            textPaint.typeface = when (textStyle) {
+                Typeface.BOLD_ITALIC -> Typeface.create(value, Typeface.BOLD_ITALIC)
+                Typeface.BOLD -> Typeface.create(value, Typeface.BOLD)
+                Typeface.ITALIC -> Typeface.create(value, Typeface.ITALIC)
+                else -> value
+            }
+            onTextPaintMeasurementChanged()
+        }
+        get() = textPaint.typeface
+
+    init {
+        var shadowColor = 0
+        var shadowDx = 0f
+        var shadowDy = 0f
+        var shadowRadius = 0f
+        var text = ""
+        var textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
+            64f, context.resources.displayMetrics)
+
+        fun applyTypedArray(arr: TypedArray) {
+            gravity = arr.getInt(R.styleable.RollingTextView_android_gravity, gravity)
+            shadowColor = arr.getColor(R.styleable.RollingTextView_android_shadowColor, shadowColor)
+            shadowDx = arr.getFloat(R.styleable.RollingTextView_android_shadowDx, shadowDx)
+            shadowDy = arr.getFloat(R.styleable.RollingTextView_android_shadowDy, shadowDy)
+            shadowRadius = arr.getFloat(R.styleable.RollingTextView_android_shadowRadius, shadowRadius)
+            text = arr.getString(R.styleable.RollingTextView_android_text) ?: ""
+            textColor = arr.getColor(R.styleable.RollingTextView_android_textColor, textColor)
+            textSize = arr.getDimension(R.styleable.RollingTextView_android_textSize, textSize)
+            textStyle = arr.getInt(R.styleable.RollingTextView_android_textStyle, textStyle)
+        }
+
+        val arr = context.obtainStyledAttributes(attrs, R.styleable.RollingTextView,
+            defStyleAttr, defStyleRes)
+
+        val textAppearanceResId = arr.getResourceId(
+            R.styleable.RollingTextView_android_textAppearance, -1)
+
+        if (textAppearanceResId != -1) {
+            val textAppearanceArr = context.obtainStyledAttributes(
+                textAppearanceResId, R.styleable.RollingTextView)
+            applyTypedArray(textAppearanceArr)
+            textAppearanceArr.recycle()
+        }
+
+        applyTypedArray(arr)
+
+        animationDuration = arr.getInt(R.styleable.RollingTextView_duration, animationDuration.toInt()).toLong()
+
+        textPaint.isAntiAlias = true
+        if (shadowColor != 0) {
+            textPaint.setShadowLayer(shadowRadius, shadowDx, shadowDy, shadowColor)
+        }
+        if (textStyle != 0) {
+            typeface = textPaint.typeface
+        }
+
+        setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
+        setText(text, false)
+
+        arr.recycle()
+
+        animator.addUpdateListener {
+            textManager.updateAnimation(it.animatedFraction)
+            checkForReLayout()
+            invalidate()
+        }
+        animator.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animation: Animator?) {
+                textManager.onAnimationEnd()
+            }
+        })
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        super.onDraw(canvas)
+
+        canvas.save()
+
+        realignAndClipCanvasForGravity(canvas)
+
+        canvas.translate(0f, textManager.textBaseline)
+
+        textManager.draw(canvas)
+
+        canvas.restore()
+    }
+
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        lastMeasuredDesiredWidth = computeDesiredWidth()
+        lastMeasuredDesiredHeight = computeDesiredHeight()
+
+        val desiredWidth = resolveSize(lastMeasuredDesiredWidth, widthMeasureSpec)
+        val desiredHeight = resolveSize(lastMeasuredDesiredHeight, heightMeasureSpec)
+
+        setMeasuredDimension(desiredWidth, desiredHeight)
+    }
+
+    override fun onSizeChanged(width: Int, height: Int, oldw: Int, oldh: Int) {
+        super.onSizeChanged(width, height, oldw, oldh)
+        viewBounds.set(paddingLeft, paddingTop, width - paddingRight,
+            height - paddingBottom)
+    }
+
+    private fun checkForReLayout(): Boolean {
+        //        val widthChanged = lastMeasuredDesiredWidth != computeDesiredWidth()
+        //        val heightChanged = lastMeasuredDesiredHeight != computeDesiredHeight()
+        //
+        //        if (widthChanged || heightChanged) {
+        //            requestLayout()
+        //            return true
+        //        }
+        //        return false
+        requestLayout()
+        return true
+    }
+
+    private fun computeDesiredWidth(): Int {
+        return textManager.currentTextWidth.toInt() + paddingLeft + paddingRight
+    }
+
+    private fun computeDesiredHeight(): Int {
+        return textManager.textHeight.toInt() + paddingTop + paddingBottom
+    }
+
+    private fun realignAndClipCanvasForGravity(canvas: Canvas) {
+        val currentWidth = textManager.currentTextWidth
+        val currentHeight = textManager.textHeight
+        val availableWidth = viewBounds.width()
+        val availableHeight = viewBounds.height()
+        var translationX = 0f
+        var translationY = 0f
+        if (gravity and Gravity.CENTER_VERTICAL == Gravity.CENTER_VERTICAL) {
+            translationY = viewBounds.top + (availableHeight - currentHeight) / 2f
+        }
+        if (gravity and Gravity.CENTER_HORIZONTAL == Gravity.CENTER_HORIZONTAL) {
+            translationX = viewBounds.left + (availableWidth - currentWidth) / 2f
+        }
+        if (gravity and Gravity.TOP == Gravity.TOP) {
+            translationY = viewBounds.top.toFloat()
+        }
+        if (gravity and Gravity.BOTTOM == Gravity.BOTTOM) {
+            translationY = viewBounds.top + (availableHeight - currentHeight)
+        }
+        if (gravity and Gravity.START == Gravity.START) {
+            translationX = viewBounds.left.toFloat()
+        }
+        if (gravity and Gravity.END == Gravity.END) {
+            translationX = viewBounds.left + (availableWidth - currentWidth)
+        }
+
+        canvas.translate(translationX, translationY)
+        canvas.clipRect(0f, 0f, currentWidth, currentHeight)
+    }
+
+    private fun onTextPaintMeasurementChanged() {
+        textManager.updateFontMatrics()
+        checkForReLayout()
+        invalidate()
+    }
+
+    /***************************** Public API below ***********************************************/
+
+    var animationInterpolator: Interpolator = LinearInterpolator()
+
+    /**
+     * @param text 设置文本
+     */
+    fun setText(text: CharSequence) = setText(text, !TextUtils.isEmpty(targetText))
+
+    fun getText() = targetText
+
+    /**
+     * @param text 设置文本
+     * @param animate 是否需要滚动效果
+     */
+    fun setText(text: CharSequence, animate: Boolean) {
+        targetText = text
+        if (animate) {
+            textManager.setText(text)
+            with(animator) {
+                if (isRunning) {
+                    cancel()
+                }
+                duration = animationDuration
+                interpolator = animationInterpolator
+                //到下一次looper去开始新的动画,解决在onAnimationEnd的时候setText的问题
+                post {
+                    start()
+                }
+            }
+        } else {
+            val originalStrategy = charStrategy
+            charStrategy = Strategy.NoAnimation()
+            textManager.setText(text)
+            charStrategy = originalStrategy
+
+            textManager.onAnimationEnd()
+            checkForReLayout()
+            invalidate()
+        }
+    }
+
+    val currentText
+        get() = textManager.currentText
+
+    fun setTextSize(textSize: Float) = setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize)
+
+    fun getTextSize() = textPaint.textSize
+
+    fun setTextSize(unit: Int, size: Float) {
+        val r: Resources = context?.resources ?: Resources.getSystem()
+        textPaint.textSize = TypedValue.applyDimension(unit, size, r.displayMetrics)
+        onTextPaintMeasurementChanged()
+    }
+
+    var textColor: Int = Color.BLACK
+        set(color) {
+            if (field != color) {
+                field = color
+                textPaint.color = color
+                invalidate()
+            }
+        }
+
+    /**
+     * px between letter
+     */
+    var letterSpacingExtra: Int
+        set(value) {
+            textManager.letterSpacingExtra = value
+        }
+        get() = textManager.letterSpacingExtra
+
+    override fun getBaseline(): Int {
+        val fontMetrics = textPaint.fontMetrics
+        return (textManager.textHeight / 2 + ((fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent)).toInt()
+    }
+
+    /**
+     * 设置动画滚动策略,如:
+     *
+     * 直接更替字符串
+     * [Strategy.NoAnimation]
+     *
+     * 普通滚动
+     * [Strategy.NormalAnimation]
+     *
+     * 指定方向滚动
+     * [Strategy.SameDirectionAnimation]
+     *
+     * 进位滚动
+     * [Strategy.CarryBitAnimation]
+     */
+    var charStrategy: CharOrderStrategy
+        set(value) {
+            charOrderManager.charStrategy = value
+        }
+        get() = charOrderManager.charStrategy
+
+    /**
+     * 添加动画监听器
+     */
+    fun addAnimatorListener(listener: Animator.AnimatorListener) = animator.addListener(listener)
+
+    /**
+     * 移除动画监听器
+     */
+    fun removeAnimatorListener(listener: Animator.AnimatorListener) = animator.removeListener(listener)
+
+    /**
+     * 添加支持的序列,如[CharOrder.Number]/[CharOrder.Alphabet]
+     *
+     * 如果orderList为[2,4,6,8,0],那么从"440"到"844"的动画将会是"440"->"642"->"844"
+     *
+     * 与[charStrategy]配合使用定义动画效果
+     */
+    fun addCharOrder(orderList: CharSequence) = charOrderManager.addCharOrder(orderList.asIterable())
+
+    /**
+     * 添加支持的序列,如[CharOrder.Number]/[CharOrder.Alphabet]
+     *
+     * 如果orderList为[2,4,6,8,0],那么从"440"到"844"的动画将会是"440"->"642"->"844"
+     *
+     * 与[charStrategy]配合使用定义动画效果
+     */
+    fun addCharOrder(orderList: Iterable<Char>) = charOrderManager.addCharOrder(orderList)
+
+    /**
+     * 添加支持的序列,如[CharOrder.Number]/[CharOrder.Alphabet]
+     *
+     * 如果orderList为[2,4,6,8,0],那么从"440"到"844"的动画将会是"440"->"642"->"844"
+     *
+     * 与[charStrategy]配合使用定义动画效果
+     */
+    fun addCharOrder(orderList: Array<Char>) = charOrderManager.addCharOrder(orderList.asIterable())
+}

+ 140 - 0
home/src/main/java/com/yingyangfly/home/widget/TextColumn.kt

@@ -0,0 +1,140 @@
+package com.yingyangfly.home.widget
+
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.widget.LinearLayout.HORIZONTAL
+import com.yingyangfly.home.widget.TextManager.Companion.EMPTY
+import com.yingyangfly.home.widget.strategy.Direction
+
+/**
+ * @author YvesCheung
+ * 2018/2/26
+ */
+@Suppress("MemberVisibilityCanBePrivate")
+internal class TextColumn(
+    private val manager: TextManager,
+    private val textPaint: Paint,
+    var changeCharList: List<Char>,
+    var direction: Direction
+) {
+
+    var currentWidth: Float = 0f
+
+    var currentChar: Char = EMPTY
+        private set
+
+    val sourceChar
+        get() = if (changeCharList.size < 2) EMPTY else changeCharList.first()
+
+    val targetChar
+        get() = if (changeCharList.isEmpty()) EMPTY else changeCharList.last()
+
+    private var sourceWidth = 0f
+
+    private var targetWidth = 0f
+
+    private var previousEdgeDelta = 0.0
+    private var edgeDelta = 0.0
+
+    var index = 0
+
+    private var firstNotEmptyChar: Char = EMPTY
+    private var firstCharWidth: Float = 0f
+    private var lastNotEmptyChar: Char = EMPTY
+    private var lastCharWidth: Float = 0f
+
+    init {
+        initChangeCharList()
+    }
+
+    fun measure() {
+        sourceWidth = manager.charWidth(sourceChar, textPaint)
+        targetWidth = manager.charWidth(targetChar, textPaint)
+        currentWidth = Math.max(sourceWidth, firstCharWidth)
+    }
+
+    fun setChangeCharList(charList: List<Char>, dir: Direction) {
+        changeCharList = charList
+        direction = dir
+        initChangeCharList()
+        index = 0
+        previousEdgeDelta = edgeDelta
+        edgeDelta = 0.0
+    }
+
+    private fun initChangeCharList() {
+        //没有动画的情况
+        if (changeCharList.size < 2) {
+            currentChar = targetChar
+        }
+        firstNotEmptyChar = changeCharList.firstOrNull { it != EMPTY } ?: EMPTY
+        firstCharWidth = manager.charWidth(firstNotEmptyChar, textPaint)
+        lastNotEmptyChar = changeCharList.lastOrNull { it != EMPTY } ?: EMPTY
+        lastCharWidth = manager.charWidth(lastNotEmptyChar, textPaint)
+        //重新计算字符宽度
+        measure()
+    }
+
+    fun onAnimationUpdate(
+        currentIndex: Int,
+        offsetPercentage: Double,
+        progress: Double
+    ): PreviousProgress {
+
+        //当前字符
+        index = currentIndex
+        currentChar = changeCharList[currentIndex]
+
+        //从上一次动画结束时的偏移值开始
+        val additionalDelta = previousEdgeDelta * (1.0 - progress)
+        edgeDelta =
+            if (direction.orientation == HORIZONTAL) {
+                offsetPercentage * currentWidth * direction.value + additionalDelta
+            } else {
+                offsetPercentage * manager.textHeight * direction.value + additionalDelta
+            }
+
+        //计算当前字符宽度,为第一个字符和最后一个字符的过渡宽度
+        currentWidth = if (currentChar.toInt() > 0) {
+            (lastCharWidth - firstCharWidth) * progress.toFloat() + firstCharWidth
+        } else {
+            0f
+        }
+
+        return PreviousProgress(index, offsetPercentage, progress, currentChar, currentWidth)
+    }
+
+    fun onAnimationEnd() {
+        currentChar = targetChar
+        edgeDelta = 0.0
+        previousEdgeDelta = 0.0
+    }
+
+    fun draw(canvas: Canvas) {
+        val cs = canvas.save()
+        val originRect = canvas.clipBounds
+        canvas.clipRect(0, originRect.top, currentWidth.toInt(), originRect.bottom)
+
+
+        fun drawText(idx: Int, horizontalOffset: Float = 0f, verticalOffset: Float = 0f) {
+
+            fun charAt(idx: Int) = CharArray(1) { changeCharList[idx] }
+
+            if (idx >= 0 && idx < changeCharList.size && changeCharList[idx] != EMPTY) {
+                canvas.drawText(charAt(idx), 0, 1, horizontalOffset, verticalOffset, textPaint)
+            }
+        }
+
+        if (direction.orientation == HORIZONTAL) {
+            drawText(index + 1, horizontalOffset = edgeDelta.toFloat() - currentWidth * direction.value)
+            drawText(index, horizontalOffset = edgeDelta.toFloat())
+            drawText(index - 1, horizontalOffset = edgeDelta.toFloat() + currentWidth * direction.value)
+        } else {
+            drawText(index + 1, verticalOffset = edgeDelta.toFloat() - manager.textHeight * direction.value)
+            drawText(index, verticalOffset = edgeDelta.toFloat())
+            drawText(index - 1, verticalOffset = edgeDelta.toFloat() + manager.textHeight * direction.value)
+        }
+
+        canvas.restoreToCount(cs)
+    }
+}

+ 137 - 0
home/src/main/java/com/yingyangfly/home/widget/TextManager.kt

@@ -0,0 +1,137 @@
+package com.yingyangfly.home.widget
+
+import android.graphics.Canvas
+import android.graphics.Paint
+import java.util.*
+
+/**
+ * @author YvesCheung
+ * 2018/2/26
+ */
+internal class TextManager(
+    private val textPaint: Paint,
+    private val charOrderManager: CharOrderManager
+) {
+
+    companion object {
+        const val EMPTY: Char = 0.toChar()
+
+        const val FLT_EPSILON: Float = 1.192092896e-07F
+    }
+
+    private val map: MutableMap<Char, Float> = LinkedHashMap(36)
+
+    private val textColumns = mutableListOf<TextColumn>()
+
+    private var charListColumns: List<List<Char>> = Collections.emptyList()
+
+    var letterSpacingExtra: Int = 0
+
+    init {
+        updateFontMatrics()
+    }
+
+    fun charWidth(c: Char, textPaint: Paint): Float {
+        return if (c == EMPTY) {
+            0f
+        } else {
+            map[c] ?: textPaint.measureText(c.toString()).also { map[c] = it }
+        }
+    }
+
+    fun updateFontMatrics() {
+        map.clear()
+        with(textPaint.fontMetrics) {
+            textHeight = bottom - top
+            textBaseline = -top
+        }
+        textColumns.forEach { it.measure() }
+    }
+
+    fun updateAnimation(progress: Float) {
+        //当changeCharList.size大于7位数的时候 有可能使Float溢出 所以要用Double
+        val initialize = PreviousProgress(0, 0.0, progress.toDouble())
+        textColumns.foldRightIndexed(initialize) { index, column, previousProgress ->
+            val nextProgress = charOrderManager.getProgress(previousProgress, index,
+                charListColumns, column.index)
+
+            val previous = column.onAnimationUpdate(nextProgress.currentIndex,
+                nextProgress.offsetPercentage, nextProgress.progress)
+            previous
+        }
+    }
+
+    fun onAnimationEnd() {
+        textColumns.forEach { it.onAnimationEnd() }
+        charOrderManager.afterCharOrder()
+    }
+
+    fun draw(canvas: Canvas) {
+        textColumns.forEach {
+            it.draw(canvas)
+            canvas.translate(it.currentWidth + letterSpacingExtra, 0f)
+        }
+    }
+
+    val currentTextWidth: Float
+        get() {
+            val space = letterSpacingExtra * Math.max(0, textColumns.size - 1)
+            val textWidth = textColumns
+                .map { it.currentWidth }
+                .fold(0f) { total, next -> total + next }
+            return textWidth + space
+        }
+
+    private fun Float.isZero(): Boolean = this < FLT_EPSILON && this > -FLT_EPSILON
+
+    fun setText(targetText: CharSequence) {
+
+//        val itr = textColumns.iterator()
+//        while (itr.hasNext()) {
+//            val column = itr.next()
+//            if (column.currentWidth.isZero()) {
+//                itr.remove()
+//            }
+//        }
+
+        val sourceText = String(currentText)
+
+        val maxLen = Math.max(sourceText.length, targetText.length)
+
+        charOrderManager.beforeCharOrder(sourceText, targetText)
+        for (idx in 0 until maxLen) {
+            val (list, direction) = charOrderManager.findCharOrder(sourceText, targetText, idx)
+            if (idx >= maxLen - sourceText.length) {
+                textColumns[idx].setChangeCharList(list, direction)
+            } else {
+                textColumns.add(idx, TextColumn(this, textPaint, list, direction))
+            }
+        }
+        charListColumns = textColumns.map { it.changeCharList }
+    }
+
+    val currentText
+        get(): CharArray = CharArray(textColumns.size) { index -> textColumns[index].currentChar }
+
+    var textHeight: Float = 0f
+        private set
+
+    var textBaseline = 0f
+        private set(value) {
+            field = value
+        }
+}
+
+data class PreviousProgress(
+    val currentIndex: Int,
+    val offsetPercentage: Double,
+    val progress: Double,
+    val currentChar: Char = TextManager.EMPTY,
+    val currentWidth: Float = 0f
+)
+
+data class NextProgress(
+    val currentIndex: Int,
+    val offsetPercentage: Double,
+    val progress: Double
+)

+ 49 - 0
home/src/main/java/com/yingyangfly/home/widget/strategy/AlignAnimationStrategy.kt

@@ -0,0 +1,49 @@
+package com.yingyangfly.home.widget.strategy
+
+import com.yingyangfly.home.widget.TextManager
+import kotlin.math.max
+import kotlin.math.roundToInt
+
+/**
+ * @author YvesCheung
+ * 2019/4/29
+ */
+@Suppress("MemberVisibilityCanBePrivate")
+open class AlignAnimationStrategy(val alignment: TextAlignment) : NormalAnimationStrategy() {
+
+    override fun findCharOrder(
+        sourceText: CharSequence,
+        targetText: CharSequence,
+        index: Int,
+        charPool: CharPool
+    ): Pair<List<Char>, Direction> {
+        val maxLen = max(sourceText.length, targetText.length)
+        var srcChar = TextManager.EMPTY
+        var tgtChar = TextManager.EMPTY
+
+        val srcRange = getTextRange(sourceText, maxLen)
+        val tgtRange = getTextRange(targetText, maxLen)
+        if (index in srcRange) {
+            srcChar = sourceText[index - srcRange.first]
+        }
+        if (index in tgtRange) {
+            tgtChar = targetText[index - tgtRange.first]
+        }
+
+        return findCharOrder(srcChar, tgtChar, index, charPool)
+    }
+
+    private fun getTextRange(text: CharSequence, maxLen: Int): IntRange {
+        val from: Int = when (alignment) {
+            TextAlignment.Left -> 0
+            TextAlignment.Center -> ((maxLen - text.length) / 2f).roundToInt()
+            TextAlignment.Right -> maxLen - text.length
+        }
+        val to: Int = from + text.length
+        return from until to
+    }
+
+    enum class TextAlignment {
+        Left, Right, Center
+    }
+}

+ 177 - 0
home/src/main/java/com/yingyangfly/home/widget/strategy/CarryBitStrategy.kt

@@ -0,0 +1,177 @@
+package com.yingyangfly.home.widget.strategy
+
+import com.yingyangfly.home.widget.NextProgress
+import com.yingyangfly.home.widget.PreviousProgress
+import com.yingyangfly.home.widget.TextManager
+import com.yingyangfly.home.widget.util.CircularList
+import com.yingyangfly.home.widget.util.ExtraList
+import kotlin.math.abs
+import kotlin.math.max
+
+/**
+ * @author YvesCheung
+ * 2018/3/4
+ */
+@Suppress("MemberVisibilityCanBePrivate")
+open class CarryBitStrategy(val direction: Direction) : SimpleCharOrderStrategy() {
+
+    protected var sourceIndex: IntArray? = null
+    protected var targetIndex: IntArray? = null
+    protected var sourceCumulative: IntArray? = null
+    protected var targetCumulative: IntArray? = null
+    protected var charOrderList: List<Collection<Char>>? = null
+    protected var toBigger: Boolean = true
+
+    override fun beforeCompute(sourceText: CharSequence, targetText: CharSequence, charPool: CharPool) {
+
+        if (sourceText.length >= 10 || targetText.length >= 10) {
+            throw IllegalStateException("your text is too long, it may overflow the integer calculation." +
+                    " please use other animation strategy.")
+        }
+
+        val maxLen = max(sourceText.length, targetText.length)
+        val srcArray = IntArray(maxLen)
+        val tgtArray = IntArray(maxLen)
+        val carryArray = IntArray(maxLen)
+        val charOrderList = mutableListOf<Collection<Char>>()
+        (0 until maxLen).forEach { index ->
+            var srcChar = TextManager.EMPTY
+            var tgtChar = TextManager.EMPTY
+            val sIdx = index - maxLen + sourceText.length
+            val tIdx = index - maxLen + targetText.length
+            if (sIdx >= 0) {
+                srcChar = sourceText[sIdx]
+            }
+            if (tIdx >= 0) {
+                tgtChar = targetText[tIdx]
+            }
+            val iterable = charPool.find { it.contains(srcChar) && it.contains(tgtChar) }
+                    ?: throw IllegalStateException("the char $srcChar or $tgtChar cannot be found in the charPool," +
+                            "please addCharOrder() before use")
+            charOrderList.add(iterable)
+            srcArray[index] = max(iterable.indexOf(srcChar) - 1, -1)
+            tgtArray[index] = max(iterable.indexOf(tgtChar) - 1, -1)
+            carryArray[index] = iterable.size - 1
+        }
+
+        val sourceCumulative = IntArray(maxLen)
+        val targetCumulative = IntArray(maxLen)
+        var srcTotal = 0
+        var tgtTotal = 0
+        var carry = 0
+        (0 until maxLen).forEach { idx ->
+            srcTotal = max(srcArray[idx], 0) + carry * srcTotal
+            tgtTotal = max(tgtArray[idx], 0) + carry * tgtTotal
+            carry = carryArray[idx]
+            sourceCumulative[idx] = srcTotal
+            targetCumulative[idx] = tgtTotal
+        }
+
+        this.sourceIndex = srcArray
+        this.targetIndex = tgtArray
+        this.sourceCumulative = sourceCumulative
+        this.targetCumulative = targetCumulative
+        this.charOrderList = charOrderList
+        this.toBigger = srcTotal < tgtTotal
+    }
+
+    override fun afterCompute() {
+        sourceCumulative = null
+        targetCumulative = null
+        charOrderList = null
+        sourceIndex = null
+        targetIndex = null
+    }
+
+    override fun nextProgress(
+        previousProgress: PreviousProgress,
+        columnIndex: Int,
+        columns: List<List<Char>>,
+        charIndex: Int): NextProgress {
+
+        /**
+         * 最低位 线性变化
+         */
+        if (columnIndex == columns.size - 1) {
+            return super.nextProgress(previousProgress, columnIndex, columns, charIndex)
+        }
+
+        val srcIndex = sourceIndex
+        val charOrders = charOrderList
+        if (srcIndex != null && charOrders != null) {
+            val preStartIndex = max(srcIndex[columnIndex + 1], 0)
+            val preCarry = charOrders[columnIndex + 1].size - 1
+            val preCurrentIndex = previousProgress.currentIndex
+            val nextStartIndex = if (toBigger) {
+                (preCurrentIndex + preStartIndex) / preCarry
+            } else {
+                (preCurrentIndex - preStartIndex - 1 + preCarry) / preCarry
+            }
+            val upgrade = if (toBigger) {
+                (preCurrentIndex + preStartIndex + 1) % preCarry == 0
+            } else {
+                (preCurrentIndex - preStartIndex) % preCarry == 0
+            }
+            return if (upgrade) {
+                NextProgress(nextStartIndex, previousProgress.offsetPercentage, previousProgress.progress)
+            } else {
+                NextProgress(nextStartIndex, 0.0, previousProgress.progress)
+            }
+        }
+        return super.nextProgress(previousProgress, columnIndex, columns, charIndex)
+    }
+
+    override fun findCharOrder(
+            sourceText: CharSequence,
+            targetText: CharSequence,
+            index: Int,
+            charPool: CharPool
+    ): Pair<List<Char>, Direction> {
+
+        val srcIndex = sourceIndex
+        val tgtIndex = targetIndex
+        val srcCumulate = sourceCumulative
+        val tgtCumulate = targetCumulative
+        val charOrders = charOrderList
+        if (srcCumulate != null && tgtCumulate != null
+                && charOrders != null && srcIndex != null && tgtIndex != null) {
+
+            val orderList = charOrders[index].filterIndexed { i, _ -> i > 0 }
+
+            val size = abs(srcCumulate[index] - tgtCumulate[index]) + 1
+            var first: Char? = null
+            var last: Char? = null
+            if (srcIndex[index] == -1) first = TextManager.EMPTY
+            if (tgtIndex[index] == -1) last = TextManager.EMPTY
+            val (list, firstIndex) = determineCharOrder(orderList, srcIndex[index].coerceAtLeast(0))
+            return circularList(
+                    rawList = list,
+                    size = size,
+                    firstIndex = firstIndex,
+                    first = first,
+                    last = last
+            ) to determineDirection()
+        }
+        throw IllegalStateException("CarryBitStrategy is in a illegal state, check it's lifecycle")
+    }
+
+    open fun circularList(
+            rawList: List<Char>,
+            size: Int,
+            firstIndex: Int,
+            first: Char?,
+            last: Char?): List<Char> {
+        val circularList = CircularList(rawList, size, firstIndex)
+        return ExtraList(circularList, first, last)
+    }
+
+    open fun determineCharOrder(orderList: List<Char>, index: Int): Pair<List<Char>, Int> {
+        return if (toBigger) {
+            orderList to index
+        } else {
+            orderList.asReversed() to (orderList.size - 1 - index)
+        }
+    }
+
+    open fun determineDirection() = direction
+}

+ 173 - 0
home/src/main/java/com/yingyangfly/home/widget/strategy/CharOrderStrategy.kt

@@ -0,0 +1,173 @@
+package com.yingyangfly.home.widget.strategy
+
+import android.widget.LinearLayout.HORIZONTAL
+import android.widget.LinearLayout.VERTICAL
+import com.yingyangfly.home.widget.NextProgress
+import com.yingyangfly.home.widget.PreviousProgress
+import com.yingyangfly.home.widget.TextManager
+import kotlin.math.max
+
+/**
+ * 字符滚动变化的策略
+ * strategy to determine how characters change
+ *
+ * @author YvesCheung
+ * 2018/2/28
+ */
+interface CharOrderStrategy {
+
+    /**
+     * 在滚动动画计算前回调,可以做初始化的事情
+     *
+     * Notifies the animation calculation will start immediately,
+     * you can override this method and do some initialization work
+     *
+     * @param sourceText 原来的文本
+     * @param targetText 动画后的目标文本
+     * @param charPool 外部设定的可选的字符变化序列
+     */
+    fun beforeCompute(sourceText: CharSequence, targetText: CharSequence, charPool: CharPool) {}
+
+    /**
+     * 从[sourceText]滚动变化到[targetText],对于索引[index]的位置,给出变化的字符顺序
+     *
+     * 也可以直接继承[SimpleCharOrderStrategy],可以更简单的实现策略
+     *
+     * you need to override this method to tell me how the animation should behave.
+     * this method will be invoked many times with different index, which is from 0 to
+     * max(sourceText.length, targetText.length)
+     *
+     * @param sourceText 原来的文本
+     * @param targetText 动画后的目标文本
+     * @param index 当前字符的位置 范围[0,Math.max(sourceText.length,targetText.length)]
+     * @param charPool 外部设定的可选的字符变化序列
+     */
+    fun findCharOrder(sourceText: CharSequence,
+                      targetText: CharSequence,
+                      index: Int,
+                      charPool: CharPool
+    ): Pair<List<Char>, Direction>
+
+
+    fun nextProgress(
+        previousProgress: PreviousProgress,
+        columnIndex: Int,
+        columns: List<List<Char>>,
+        charIndex: Int): NextProgress
+
+    /**
+     * 在滚动动画计算后回调
+     *
+     * you can override this method to clean up after animation
+     */
+    fun afterCompute() {}
+}
+
+/**
+ * 简单的策略模版:在[findCharOrder]中选择一个重写即可
+ *
+ * a simple strategy template
+ */
+abstract class SimpleCharOrderStrategy : CharOrderStrategy {
+
+    override fun beforeCompute(sourceText: CharSequence, targetText: CharSequence, charPool: CharPool) {}
+
+    override fun afterCompute() {}
+
+    override fun nextProgress(
+            previousProgress: PreviousProgress,
+            columnIndex: Int,
+            columns: List<List<Char>>,
+            charIndex: Int): NextProgress {
+
+        val columnSize = columns.size
+        val charList = columns[columnIndex]
+        val factor = getFactor(previousProgress, columnIndex, columnSize, charList)
+        //相对于字符序列的进度
+        val sizeProgress = (charList.size - 1) * previousProgress.progress
+
+        //通过进度获得当前字符
+        val currentCharIndex = sizeProgress.toInt()
+
+        //求底部偏移值
+        val k = 1.0 / factor
+        val b = (1.0 - factor) * k
+        val offset = sizeProgress - currentCharIndex
+        val offsetPercentage = if (offset >= 1.0 - factor) offset * k - b else 0.0
+
+        return NextProgress(currentCharIndex, offsetPercentage, previousProgress.progress)
+    }
+
+    open fun getFactor(previousProgress: PreviousProgress,
+                       index: Int,
+                       size: Int,
+                       charList: List<Char>): Double = 1.0
+
+    override fun findCharOrder(
+            sourceText: CharSequence,
+            targetText: CharSequence,
+            index: Int,
+            charPool: CharPool
+    ): Pair<List<Char>, Direction> {
+
+        val maxLen = max(sourceText.length, targetText.length)
+        val disSrc = maxLen - sourceText.length
+        val disTgt = maxLen - targetText.length
+
+        var srcChar = TextManager.EMPTY
+        var tgtChar = TextManager.EMPTY
+        if (index >= disSrc) {
+            srcChar = sourceText[index - disSrc]
+        }
+        if (index >= disTgt) {
+            tgtChar = targetText[index - disTgt]
+        }
+
+        return findCharOrder(srcChar, tgtChar, index, charPool)
+    }
+
+    /**
+     * 从字符[sourceChar]滚动变化到[targetChar]的变化顺序
+     *
+     * @param sourceChar 原字符
+     * @param targetChar 滚动变化后的目标字符
+     * @param index 字符索引
+     * @param charPool 外部设定的序列,如果没设定则为空
+     */
+    open fun findCharOrder(sourceChar: Char, targetChar: Char, index: Int, charPool: CharPool)
+            : Pair<List<Char>, Direction> {
+        val iterable = charPool.find { it.contains(sourceChar) && it.contains(targetChar) }
+        return findCharOrder(sourceChar, targetChar, index, iterable)
+    }
+
+    /**
+     * 从字符[sourceChar]滚动变化到[targetChar]的变化顺序
+     *
+     * @param sourceChar 原字符
+     * @param targetChar 滚动变化后的目标字符
+     * @param index 字符索引
+     * @param order 外部设定的序列,如果没设定则为空
+     */
+    open fun findCharOrder(sourceChar: Char, targetChar: Char, index: Int, order: Iterable<Char>?)
+            : Pair<List<Char>, Direction> {
+        return listOf(sourceChar, targetChar) to Direction.SCROLL_DOWN
+    }
+}
+
+typealias CharPool = List<Collection<Char>>
+
+/**
+ * 字符动画滚动的方向:
+ *
+ * [SCROLL_UP] 向上滚动
+ * [SCROLL_DOWN] 向下滚动
+ * [SCROLL_LEFT] 向左滚动
+ * [SCROLL_RIGHT] 向右滚动
+ */
+enum class Direction(val value: Int, val orientation: Int) {
+    SCROLL_UP(-1, VERTICAL),
+    SCROLL_DOWN(1, VERTICAL),
+
+    SCROLL_LEFT(-1, HORIZONTAL),
+    SCROLL_RIGHT(1, HORIZONTAL)
+}

+ 102 - 0
home/src/main/java/com/yingyangfly/home/widget/strategy/NonZeroFirstStrategy.kt

@@ -0,0 +1,102 @@
+package com.yingyangfly.home.widget.strategy
+
+import com.yingyangfly.home.widget.TextManager
+import com.yingyangfly.home.widget.util.CircularList
+import com.yingyangfly.home.widget.util.ReplaceList
+import kotlin.math.max
+
+/**
+ * @author YvesCheung
+ * 2018/3/4
+ */
+open class NonZeroFirstStrategy(private val strategy: CharOrderStrategy) : CharOrderStrategy by strategy {
+
+    private var sourceZeroFirst = true
+    private var targetZeroFirst = true
+
+    override fun beforeCompute(sourceText: CharSequence, targetText: CharSequence, charPool: CharPool) {
+        strategy.beforeCompute(sourceText, targetText, charPool)
+        sourceZeroFirst = true
+        targetZeroFirst = true
+    }
+
+    override fun findCharOrder(
+            sourceText: CharSequence,
+            targetText: CharSequence,
+            index: Int,
+            charPool: CharPool
+    ): Pair<List<Char>, Direction> {
+
+        val (list, direction) = strategy.findCharOrder(sourceText, targetText, index, charPool)
+
+        val maxLen = max(sourceText.length, targetText.length)
+        val firstIdx = firstZeroAfterEmpty(list)
+        val lastIdx = lastZeroBeforeEmpty(list)
+        var replaceFirst = false
+        var replaceLast = false
+
+        if (sourceZeroFirst && firstIdx != -1 && index != maxLen - 1) {
+            replaceFirst = true
+        } else {
+            sourceZeroFirst = false
+        }
+
+        if (targetZeroFirst && lastIdx != -1 && index != maxLen - 1) {
+            replaceLast = true
+        } else {
+            targetZeroFirst = false
+        }
+
+        var replaceList = if (replaceFirst && replaceLast) {
+            ReplaceList(list, TextManager.EMPTY, TextManager.EMPTY, { firstIdx }, { lastIdx })
+        } else if (replaceFirst) {
+            ReplaceList(list, first = TextManager.EMPTY, firstReplacePosition = { firstIdx },
+                    lastReplacePosition = { lastIdx })
+        } else if (replaceLast) {
+            ReplaceList(list, last = TextManager.EMPTY, firstReplacePosition = { firstIdx },
+                    lastReplacePosition = { lastIdx })
+        } else {
+            list
+        }
+
+        replaceList = if (replaceFirst && replaceLast) {
+            CircularList(replaceList, lastIdx - firstIdx + 1, firstIdx)
+        } else if (replaceFirst) {
+            CircularList(replaceList, replaceList.size - firstIdx, firstIdx)
+        } else if (replaceLast) {
+            CircularList(replaceList, lastIdx + 1)
+        } else {
+            replaceList
+        }
+
+        return replaceList to direction
+    }
+
+    private fun firstZeroAfterEmpty(list: List<Char>): Int {
+        for ((idx, c) in list.withIndex()) {
+            if (c == '0') {
+                return idx
+            }
+            if (c != TextManager.EMPTY) {
+                break
+            }
+        }
+        return -1
+    }
+
+    private fun lastZeroBeforeEmpty(list: List<Char>): Int {
+        val iter = list.listIterator(list.size)
+        var idx = list.size
+        while (iter.hasPrevious()) {
+            val c = iter.previous()
+            idx--
+            if (c == '0') {
+                return idx
+            }
+            if (c != TextManager.EMPTY) {
+                break
+            }
+        }
+        return -1
+    }
+}

+ 36 - 0
home/src/main/java/com/yingyangfly/home/widget/strategy/NormalAnimationStrategy.kt

@@ -0,0 +1,36 @@
+package com.yingyangfly.home.widget.strategy
+
+/**
+ * @author YvesCheung
+ * 2018/3/4
+ */
+open class NormalAnimationStrategy : SimpleCharOrderStrategy() {
+
+    override fun findCharOrder(
+            sourceChar: Char,
+            targetChar: Char,
+            index: Int,
+            order: Iterable<Char>?): Pair<List<Char>, Direction> {
+
+        return if (sourceChar == targetChar) {
+            listOf(targetChar) to Direction.SCROLL_DOWN
+
+        } else if (order == null) {
+            listOf(sourceChar, targetChar) to Direction.SCROLL_DOWN
+
+        } else {
+            val srcIndex = order.indexOf(sourceChar)
+            val tgtIndex = order.indexOf(targetChar)
+
+            if (srcIndex < tgtIndex) {
+                order.subList(srcIndex, tgtIndex) to Direction.SCROLL_DOWN
+            } else {
+                order.subList(tgtIndex, srcIndex).asReversed() to Direction.SCROLL_UP
+            }
+        }
+    }
+
+    private fun <T> Iterable<T>.subList(start: Int, end: Int): List<T> {
+        return this.filterIndexed { index, _ -> index in start..end }
+    }
+}

+ 21 - 0
home/src/main/java/com/yingyangfly/home/widget/strategy/SameDirectionStrategy.kt

@@ -0,0 +1,21 @@
+package com.yingyangfly.home.widget.strategy
+
+/**
+ * @author YvesCheung
+ * 2018/3/5
+ */
+class SameDirectionStrategy(
+    private val direction: Direction,
+    private val otherStrategy: CharOrderStrategy = Strategy.NormalAnimation()
+) : SimpleCharOrderStrategy() {
+
+    override fun findCharOrder(
+            sourceText: CharSequence,
+            targetText: CharSequence,
+            index: Int,
+            charPool: CharPool
+    ): Pair<List<Char>, Direction> {
+
+        return otherStrategy.findCharOrder(sourceText, targetText, index, charPool).first to direction
+    }
+}

+ 21 - 0
home/src/main/java/com/yingyangfly/home/widget/strategy/StickyStrategy.kt

@@ -0,0 +1,21 @@
+package com.yingyangfly.home.widget.strategy
+
+import com.yingyangfly.home.widget.PreviousProgress
+
+/**
+ * @author YvesCheung
+ * 2018/3/6
+ */
+@Suppress("MemberVisibilityCanBePrivate")
+class StickyStrategy(val factor: Double) : NormalAnimationStrategy() {
+
+    init {
+        if (factor <= 0.0 && factor > 1.0) {
+            throw IllegalStateException("factor must be in range (0,1] but now is $factor")
+        }
+    }
+
+    override fun getFactor(previousProgress: PreviousProgress, index: Int, size: Int, charList: List<Char>): Double {
+        return factor
+    }
+}

+ 75 - 0
home/src/main/java/com/yingyangfly/home/widget/strategy/Strategy.kt

@@ -0,0 +1,75 @@
+@file:Suppress("FunctionName")
+package com.yingyangfly.home.widget.strategy
+
+import com.yingyangfly.home.widget.strategy.*
+
+/**
+ * @author YvesCheung
+ * 2018/2/28
+ */
+@Suppress("MemberVisibilityCanBePrivate")
+object Strategy {
+
+    /**
+     * 不显示动画效果
+     *
+     * the source text will be transformed directly into the target text without animation
+     */
+    @JvmStatic
+    fun NoAnimation(): CharOrderStrategy = object : SimpleCharOrderStrategy() {
+        override fun findCharOrder(sourceChar: Char, targetChar: Char, index: Int, order: Iterable<Char>?) =
+                listOf(targetChar) to Direction.SCROLL_DOWN
+    }
+
+    /**
+     * 默认的动画效果:
+     * 当调用[RollingTextView.addCharOrder]之后,在*charOder*里面的顺序存在这样的关系: **【目标字符在原字符的右边】** ,
+     * 则会有向下滚动的动画效果。如果 **【目标字符在原字符的左边】** ,则会有向上滚动的动画效果。如果目标字符和原字符不在同一个
+     * *charOrder* 中,则不会有动画效果
+     *
+     * it's default animation. a character will roll down to another character on the right and vice versa.
+     */
+    @JvmStatic
+    fun NormalAnimation(): CharOrderStrategy = NormalAnimationStrategy()
+
+    /**
+     * 指定方向滚动的动画:
+     * 与默认动画效果相似,但一定会沿指定方向滚动。见[Direction]
+     *
+     * the original character rolls in the specified direction no matter, regardless of the target character
+     * on the left or the right.
+     */
+    @JvmStatic
+    fun SameDirectionAnimation(direction: Direction): CharOrderStrategy = SameDirectionStrategy(direction)
+
+    /**
+     * 进位动画:
+     * 高位数字会在低位数字滚动到达上限的时候再滚动。比如十进制数低位滚动9的时候,下一次滚动高位数将进1
+     *
+     * the animation starts with the rightmost digits and works to the left.
+     */
+    @JvmStatic
+    fun CarryBitAnimation(direction: Direction = Direction.SCROLL_DOWN): CharOrderStrategy = NonZeroFirstAnimation(
+        CarryBitStrategy(direction)
+    )
+
+    /**
+     * 装饰者模式,使动画最高位数字不为0
+     *
+     * Decoration Pattern: to limit the number to start with 0
+     */
+    @JvmStatic
+    fun NonZeroFirstAnimation(orderStrategy: CharOrderStrategy): CharOrderStrategy =
+            NonZeroFirstStrategy(orderStrategy)
+
+
+    /**
+     * 粘稠动画:
+     * [factor]值是一个满足 (0,1] 的数字,数字越大,滚动越平滑,数字越小,滚动越跳跃
+     *
+     * parameter [factor] is a number in (0,1], which determines the fluency of animation.
+     */
+    @JvmStatic
+    fun StickyAnimation(factor: Double): CharOrderStrategy =
+            StickyStrategy(factor)
+}

+ 78 - 0
home/src/main/java/com/yingyangfly/home/widget/util/CircularList.kt

@@ -0,0 +1,78 @@
+package com.yingyangfly.home.widget.util
+
+/**
+ * 可以把原来的[list]改造成循环列表
+ *
+ * 比如原来的列表为 a = <1,2,3>,如果有 b = CircularList(a, 6), 那么 b 列表遍历结果为 <1,2,3,1,2,3>,
+ * 如果有 c = CircularList(a, 6, 1), 那么 c 列表的遍历结果为 <2,3,1,2,3,1>
+ *
+ * @param list 原来的列表
+ * @param size 新列表的总大小
+ * @param startIndex 新列表的第一个元素从原列表的startIndex索引值开始
+ *
+ * @author YvesCheung
+ * 2018/2/27
+ */
+class CircularList<T> @JvmOverloads constructor(
+    private val list: List<T>,
+    override val size: Int,
+    private val startIndex: Int = 0
+) : List<T> by list {
+
+    private val rawSize = list.size
+
+    override fun get(index: Int): T {
+        val rawIndex = (index + startIndex) % rawSize
+        return list[rawIndex]
+    }
+
+    override fun isEmpty() = size <= 0
+
+    override fun indexOf(element: T) = indexOfFirst { it == element }
+
+    override fun lastIndexOf(element: T) = indexOfLast { it == element }
+
+    override fun subList(fromIndex: Int, toIndex: Int): List<T> =
+        CircularList(list, toIndex - fromIndex, size + fromIndex)
+
+    override fun iterator(): Iterator<T> = listIterator()
+
+    override fun listIterator(): ListIterator<T> = listIterator(0)
+
+    override fun listIterator(index: Int): ListIterator<T> = CircularIterator(index)
+
+    private inner class CircularIterator(private var index: Int = 0) : ListIterator<T> {
+
+        init {
+            if (index < 0 || index > size) {
+                throw ArrayIndexOutOfBoundsException("index should be in range [0,$size] but now is $index")
+            }
+        }
+
+        override fun hasNext(): Boolean = index < size
+
+        override fun next(): T {
+            if (!hasNext()) throw NoSuchElementException()
+            return get(index++)
+        }
+
+        override fun hasPrevious(): Boolean = index > 0
+
+        override fun nextIndex(): Int = index
+
+        override fun previous(): T {
+            if (!hasPrevious()) throw NoSuchElementException()
+            return get(--index)
+        }
+
+        override fun previousIndex(): Int = index - 1
+    }
+
+    override fun toString(): String {
+        val sb = StringBuilder("[")
+        iterator().forEach {
+            sb.append("$it ")
+        }
+        return sb.append("]").toString()
+    }
+}

+ 116 - 0
home/src/main/java/com/yingyangfly/home/widget/util/ExtraList.kt

@@ -0,0 +1,116 @@
+package com.yingyangfly.home.widget.util
+
+/**
+ * 可以在原来的[list]上额外添加一个头元素[first]和一个尾元素[last]
+ *
+ * @param list 原来的list
+ * @param first 可选的头节点,为空则不会添加
+ * @param last 可选的尾节点,尾空则不会添加
+ *
+ * @author YvesCheung
+ * 2018/3/3
+ */
+class ExtraList<T>(
+        private val list: List<T>,
+        private val first: T? = null,
+        private val last: T? = null
+) : List<T> {
+
+    override val size: Int = when {
+        first != null && last != null -> list.size + 2
+        first != null || last != null -> list.size + 1
+        else -> list.size
+    }
+
+    override fun contains(element: T): Boolean {
+        return first == element || last == element || list.contains(element)
+    }
+
+    override fun containsAll(elements: Collection<T>): Boolean {
+        return elements.all { contains(it) }
+    }
+
+    override fun get(index: Int): T {
+        return when {
+            index == 0 && first != null -> first
+            index == size - 1 && last != null -> last
+            first != null ->list[index - 1]
+            else -> list[index]
+        }
+    }
+
+    override fun indexOf(element: T): Int {
+        if (first != null && first == element) {
+            return 0
+        }
+        val rawFirstIndex = list.indexOf(element)
+        return when {
+            rawFirstIndex != -1 -> {
+                when {
+                    first != null -> rawFirstIndex + 1
+                    else -> rawFirstIndex
+                }
+            }
+            last != null && last == element -> size - 1
+            else -> rawFirstIndex
+        }
+    }
+
+    override fun isEmpty(): Boolean {
+        return size <= 0
+    }
+
+    override fun iterator(): Iterator<T> = ExtraIterator()
+
+    override fun lastIndexOf(element: T): Int {
+        if (last != null && last == element) {
+            return size - 1
+        }
+        val rawLastIndex = list.lastIndexOf(element)
+        return when {
+            rawLastIndex != -1 -> {
+                when {
+                    first != null -> rawLastIndex + 1
+                    else -> rawLastIndex
+                }
+            }
+            first != null && first == element -> 0
+            else -> rawLastIndex
+        }
+    }
+
+    override fun listIterator(): ListIterator<T> = ExtraIterator()
+
+    override fun listIterator(index: Int): ListIterator<T> = ExtraIterator(index)
+
+    override fun subList(fromIndex: Int, toIndex: Int): List<T> {
+        throw IllegalStateException("Not Support")
+    }
+
+    private inner class ExtraIterator(private var index: Int = 0) : ListIterator<T> {
+
+        init {
+            if (index < 0 || index > size) {
+                throw ArrayIndexOutOfBoundsException("index should be in range [0,$size] but now is $index")
+            }
+        }
+
+        override fun hasNext() = index < size
+
+        override fun hasPrevious() = index > 0
+
+        override fun next(): T {
+            if (!hasNext()) throw NoSuchElementException()
+            return get(index++)
+        }
+
+        override fun nextIndex(): Int = index
+
+        override fun previous(): T {
+            if (!hasPrevious()) throw NoSuchElementException()
+            return get(--index)
+        }
+
+        override fun previousIndex(): Int = index - 1
+    }
+}

+ 83 - 0
home/src/main/java/com/yingyangfly/home/widget/util/ReplaceList.kt

@@ -0,0 +1,83 @@
+package com.yingyangfly.home.widget.util
+
+/**
+ * @author YvesCheung
+ * 2018/3/4
+ */
+class ReplaceList<T>(
+        val list: List<T>,
+        val first: T? = null,
+        val last: T? = null,
+        firstReplacePosition: () -> Int,
+        lastReplacePosition: () -> Int
+) : List<T> {
+
+    override val size: Int = list.size
+
+    private var firstIdx = -1
+    private var lastIdx = -1
+
+    init {
+        if (first != null) {
+            firstIdx = firstReplacePosition()
+        }
+        if (last != null) {
+            lastIdx = lastReplacePosition()
+        }
+    }
+
+    override fun contains(element: T): Boolean = any { it == element }
+
+    override fun containsAll(elements: Collection<T>): Boolean = elements.all { contains(it) }
+
+    override fun get(index: Int): T {
+        return when {
+            index == firstIdx && first != null -> first
+            index == lastIdx && last != null -> last
+            else -> list[index]
+        }
+    }
+
+    override fun indexOf(element: T): Int = indexOfFirst { it == element }
+
+    override fun isEmpty(): Boolean = size <= 0
+
+    override fun iterator(): Iterator<T> = ReplaceIterator()
+
+    override fun lastIndexOf(element: T): Int = indexOfLast { it == element }
+
+    override fun listIterator(): ListIterator<T> = ReplaceIterator()
+
+    override fun listIterator(index: Int): ListIterator<T> = ReplaceIterator(index)
+
+    override fun subList(fromIndex: Int, toIndex: Int): List<T> {
+        throw IllegalStateException("Not support")
+    }
+
+    private inner class ReplaceIterator(private var index: Int = 0) : ListIterator<T> {
+
+        init {
+            if (index < 0 || index > size) {
+                throw ArrayIndexOutOfBoundsException("index should be in range [0,$size] but now is $index")
+            }
+        }
+
+        override fun hasNext() = index < size
+
+        override fun hasPrevious() = index > 0
+
+        override fun next(): T {
+            if (!hasNext()) throw NoSuchElementException()
+            return get(index++)
+        }
+
+        override fun nextIndex(): Int = index
+
+        override fun previous(): T {
+            if (!hasPrevious()) throw NoSuchElementException()
+            return get(--index)
+        }
+
+        override fun previousIndex(): Int = index - 1
+    }
+}

+ 16 - 0
home/src/main/res/values/attrs.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <declare-styleable name="RollingTextView" tools:ignore="ResourceName">
+        <attr name="duration" format="reference|integer" />
+        <attr name="android:gravity" tools:ignore="ResourceName" />
+        <attr name="android:shadowColor" tools:ignore="ResourceName" />
+        <attr name="android:shadowDx" tools:ignore="ResourceName" />
+        <attr name="android:shadowDy" tools:ignore="ResourceName" />
+        <attr name="android:shadowRadius" tools:ignore="ResourceName" />
+        <attr name="android:text" tools:ignore="ResourceName" />
+        <attr name="android:textAppearance" tools:ignore="ResourceName" />
+        <attr name="android:textColor" tools:ignore="ResourceName" />
+        <attr name="android:textSize" tools:ignore="ResourceName" />
+        <attr name="android:textStyle" tools:ignore="ResourceName" />
+    </declare-styleable>
+</resources>