Last active
August 17, 2018 15:16
-
-
Save hector6872/a8927a78428c33e5390441683efa3e9f to your computer and use it in GitHub Desktop.
MarqueeAnimateView for Android (Kotlin)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} |
Author
hector6872
commented
Aug 17, 2018
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment