Skip to content

Instantly share code, notes, and snippets.

@hector6872
Last active August 17, 2018 15:16
Show Gist options
  • Save hector6872/a8927a78428c33e5390441683efa3e9f to your computer and use it in GitHub Desktop.
Save hector6872/a8927a78428c33e5390441683efa3e9f to your computer and use it in GitHub Desktop.
MarqueeAnimateView for Android (Kotlin)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MarqueeAnimateView">
<attr format="integer" name="pa_durationMs"/>
<attr format="dimension" name="pa_scaleBitmapTo"/>
<attr format="boolean" name="pa_reverse"/>
<attr format="boolean" name="pa_autoReverse"/>
<attr format="reference" name="pa_bitmap"/>
<attr format="enum" name="pa_state">
<enum name="start" value="0"/>
<enum name="stop" value="1"/>
</attr>
</declare-styleable>
</resources>
class MarqueeAnimateView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
companion object {
private const val STATE_START = 0
private const val STATE_STOP = 1
private const val DEFAULT_DURATION_MS = 1000
private const val DEFAULT_BITMAP_SCALE_HEIGHT_TO = Int.MAX_VALUE
private const val DEFAULT_REVERSE = false
private const val DEFAULT_REVERSE_AUTO = false
private const val DEFAULT_FPS = 60
private val DEFAULT_DISTANCE_PER_FRAME = Float.MAX_VALUE
}
private var bitmap: Bitmap? = null
private var bitmapScaleHeightTo = 0
private var bitmapHeight = 0
private val clipBoundsRect = Rect()
private var offset = 0F
private var durationMs = DEFAULT_DURATION_MS
private var distancePerFrame = 0F
private var reverse = false
private var autoReverse = false
private var isStarted = false
init {
if (!isInEditMode) {
attrs?.let {
val typedArray = context.obtainStyledAttributes(it, R.styleable.MarqueeAnimateView)
durationMs = typedArray.getInt(R.styleable.MarqueeAnimateView_pa_durationMs, DEFAULT_DURATION_MS)
autoReverse = typedArray.getBoolean(R.styleable.MarqueeAnimateView_pa_autoReverse, DEFAULT_REVERSE_AUTO)
reverse = typedArray.getBoolean(R.styleable.MarqueeAnimateView_pa_reverse, DEFAULT_REVERSE)
bitmapScaleHeightTo =
typedArray.getDimensionPixelSize(R.styleable.MarqueeAnimateView_pa_scaleBitmapTo, DEFAULT_BITMAP_SCALE_HEIGHT_TO)
setBitmap(typedArray.getResourceId(R.styleable.MarqueeAnimateView_pa_bitmap, 0))
if (typedArray.getInt(R.styleable.MarqueeAnimateView_pa_state, STATE_START) == STATE_START) start()
typedArray.recycle()
}
}
}
override fun onMeasure(
widthMeasureSpec: Int,
heightMeasureSpec: Int
) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val heightMode = View.MeasureSpec.getMode(heightMeasureSpec)
if (heightMode != MeasureSpec.EXACTLY) {
setMeasuredDimension(View.MeasureSpec.getSize(widthMeasureSpec), bitmapHeight)
}
}
override
fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
bitmap?.let { safeBitmap ->
canvas.getClipBounds(clipBoundsRect)
if (distancePerFrame == DEFAULT_DISTANCE_PER_FRAME) {
distancePerFrame = calculateDistancePerFrame(clipBoundsRect.width(), safeBitmap.width)
if (reverse) distancePerFrame = -distancePerFrame
}
var changeDirection = false
when {
distancePerFrame > 0F && offset >= 0F -> {
offset = if (autoReverse) 0F else -(safeBitmap.width.toFloat())
changeDirection = autoReverse
}
distancePerFrame < 0F && safeBitmap.width < clipBoundsRect.width() && offset <= -(safeBitmap.width) -> {
offset = if (autoReverse) -(safeBitmap.width.toFloat()) else 0F
changeDirection = autoReverse
}
distancePerFrame < 0F && safeBitmap.width > clipBoundsRect.width() && offset <= -(safeBitmap.width - clipBoundsRect.width()) -> {
offset = if (autoReverse) -(safeBitmap.width - clipBoundsRect.width().toFloat()) else offset
changeDirection = autoReverse
}
}
var left = offset
while (left < clipBoundsRect.width()) {
canvas.drawBitmap(safeBitmap, left, 0f, null)
left += safeBitmap.width
}
if (isStarted && distancePerFrame != 0F) {
if (changeDirection) distancePerFrame = -distancePerFrame
offset += distancePerFrame
postInvalidateOnAnimation()
}
}
}
private fun calculateDistancePerFrame(
totalWidth: Int,
bitmapWidth: Int
): Float {
val durationSec: Float = durationMs.toFloat() / 1000
return when {
bitmapWidth < totalWidth -> (bitmapWidth / DEFAULT_FPS) / durationSec
bitmapWidth > totalWidth -> ((bitmapWidth - totalWidth) / DEFAULT_FPS) / durationSec
else -> 0F
}
}
fun start() {
if (!isStarted) {
isStarted = true
postInvalidateOnAnimation()
}
}
fun stop() {
if (isStarted) {
isStarted = false
invalidate()
}
}
fun setReverse(reverse: Boolean) {
this.reverse = reverse
reset()
checkState()
}
fun setAutoReverse(autoReverse: Boolean) {
this.autoReverse = autoReverse
reset()
checkState()
}
fun setSpeed(speed: Int) {
this.durationMs = speed
reset()
checkState()
}
fun setBitmap(drawableResId: Int) {
this.bitmap = getScaledBitmapFromResource(drawableResId)
internalSetBitmapFromAny()
}
fun setBitmap(bitmap: Bitmap) {
this.bitmap = getScaledBitmapFromBitmap(bitmap)
internalSetBitmapFromAny()
}
private fun internalSetBitmapFromAny() {
bitmapHeight = if (bitmapScaleHeightTo != DEFAULT_BITMAP_SCALE_HEIGHT_TO) bitmapScaleHeightTo else bitmap?.height ?: 0
reset()
if (isStarted) postInvalidateOnAnimation() else invalidate()
}
private fun reset() {
offset = 0F
distancePerFrame = DEFAULT_DISTANCE_PER_FRAME
}
private fun checkState() {
if (isStarted) postInvalidateOnAnimation()
}
private fun getScaledBitmapFromResource(drawableResId: Int): Bitmap? = if (bitmapScaleHeightTo != DEFAULT_BITMAP_SCALE_HEIGHT_TO) {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeResource(resources, drawableResId, options)
options.inSampleSize = calculateInSampleSize(options)
options.inJustDecodeBounds = false
getScaledBitmapFromBitmap(BitmapFactory.decodeResource(resources, drawableResId, options))
} else {
BitmapFactory.decodeResource(resources, drawableResId)
}
private fun calculateInSampleSize(
options: Options
): Int {
val originalHeight = options.outHeight
var inSampleSize = 1
if (originalHeight > bitmapScaleHeightTo) {
val halfHeight = originalHeight / 2
while (halfHeight / inSampleSize >= bitmapScaleHeightTo) {
inSampleSize *= 2
}
}
return inSampleSize
}
private fun getScaledBitmapFromBitmap(bitmap: Bitmap?): Bitmap? {
if (bitmap == null) return null
return if (bitmapScaleHeightTo != DEFAULT_BITMAP_SCALE_HEIGHT_TO) {
val factor = bitmapScaleHeightTo.toFloat() / bitmap.height
Bitmap.createScaledBitmap(bitmap, (bitmap.width * factor).toInt(), bitmapScaleHeightTo, true)
} else {
bitmap
}
}
}
@hector6872
Copy link
Author

screenrecording

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment