Created
February 27, 2022 05:31
-
-
Save shirobutton/accf88001baf67e6353118f863fd5296 to your computer and use it in GitHub Desktop.
BitmapToNinePatchConverter
This file contains 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
object BitmapToNinePatchConverter { | |
operator fun invoke(resources: Resources, bitmap: Bitmap): NinePatchDrawable { | |
val trimmedBitmap = trimBitmap(bitmap) | |
val info = NinePatchBorderReader(bitmap).getPatchInfo() | |
return NinePatchDrawable( | |
resources, | |
trimmedBitmap, | |
info.ninePatchChunk, | |
info.padding, | |
"" | |
) | |
} | |
private fun trimBitmap(bitmap: Bitmap) = bitmap.let { | |
Bitmap.createBitmap(bitmap, 1, 1, it.width - 2, it.height - 2) | |
} | |
} | |
private class NinePatchBorderReader(private val bitmap: Bitmap) { | |
private val imageWidth = bitmap.width - 2 | |
private val imageHeight = bitmap.height - 2 | |
fun getPatchInfo(): NinePatchInfo { | |
val xDividers = getOneSideDividers(imageWidth,::horizontalPatchChecker) | |
val yDividers = getOneSideDividers(imageHeight, ::verticalPatchChecker) | |
return NinePatchInfo(xDividers, yDividers, getPadding()) | |
} | |
// 9-patch border starts at 1 index of bitmap | |
// bitmap-indexes: 0, 1, 2, 3, 4 | |
// 9patch-indexes: 0, 1, 2 | |
private fun horizontalPatchChecker(x: Int, y:Int = 0) = bitmap.isPatchPosition(x + 1, y) | |
private fun verticalPatchChecker(y: Int, x:Int = 0) = bitmap.isPatchPosition(x, y + 1) | |
private tailrec fun getOneSideDividers( | |
imageSideSize: Int, | |
patchChecker: (Int) -> Boolean, | |
beforeIsPatched: Boolean = false, | |
currentBorderIndex: Int = 0, | |
dividers: List<Int> = emptyList() | |
): List<Int> { | |
if(currentBorderIndex > imageSideSize) return dividers | |
val currentPositionIsPatched = patchChecker(currentBorderIndex) | |
val isDivider = beforeIsPatched != currentPositionIsPatched | |
val nextDividers = when { | |
isDivider -> dividers + currentBorderIndex | |
else -> dividers | |
} | |
return getOneSideDividers( | |
imageSideSize, | |
patchChecker, | |
currentPositionIsPatched, | |
currentBorderIndex + 1, | |
nextDividers | |
) | |
} | |
private fun getPadding(): Rect { | |
val (left, right) = getOneSidePaddings(imageWidth) { | |
horizontalPatchChecker(it, imageHeight + 1) | |
} | |
val (top, bottom) = getOneSidePaddings(imageHeight) { | |
verticalPatchChecker(it, imageWidth + 1) | |
} | |
return Rect(left, top, right, bottom) | |
} | |
private fun getOneSidePaddings(size: Int, checkPatch: (Int) -> Boolean) = | |
(0..size).let { it.first(checkPatch) to size - it.last(checkPatch) - 1 } | |
private fun Bitmap.isPatchPosition(x: Int, y: Int) = this.getPixel(x, y) == Color.BLACK | |
} | |
private class NinePatchInfo( | |
private val xDividers: List<Int>, | |
private val yDividers: List<Int>, | |
val padding: Rect | |
) { | |
val ninePatchChunk: ByteArray | |
get() { | |
val buffer = ByteBuffer.allocate(bufferCapacity).order(ByteOrder.nativeOrder()) | |
buffer.put(WAS_SERIALISED) | |
.put((xDividers.size).toByte()) | |
.put((yDividers.size).toByte()) | |
.put(colorSize.toByte()) | |
.position(buffer.position() + BEFORE_PADDING_SKIP_BYTE_SIZE) | |
buffer.putInt(padding.left) | |
buffer.putInt(padding.right) | |
buffer.putInt(padding.top) | |
buffer.putInt(padding.bottom) | |
buffer.position(buffer.position() + AFTER_PADDING_SKIP_BYTE_SIZE) | |
xDividers.forEach(buffer::putInt) | |
yDividers.forEach(buffer::putInt) | |
repeat(colorSize) { buffer.putInt(NO_COLOR) } | |
return buffer.array() | |
} | |
private val colorSize = xDividers.size * yDividers.size | |
private val bufferCapacity: Int | |
get() { | |
val xDividersByteSize = ONE_DIVISION_SIZE * xDividers.size | |
val yDividersByteSize = ONE_DIVISION_SIZE * yDividers.size | |
val colorByteSize = colorSize * ONE_DIVISION_SIZE | |
return ( | |
HEAD_BYTE_SIZE + | |
BEFORE_PADDING_SKIP_BYTE_SIZE + | |
PADDINGS_BYTE_SIZE + | |
AFTER_PADDING_SKIP_BYTE_SIZE + | |
xDividersByteSize + | |
yDividersByteSize + | |
colorByteSize | |
) | |
} | |
companion object { | |
private const val NO_COLOR= 1 | |
private const val WAS_SERIALISED: Byte = 1 | |
private const val HEAD_BYTE_SIZE = 4 | |
private const val BEFORE_PADDING_SKIP_BYTE_SIZE = 8 | |
private const val PADDINGS_BYTE_SIZE = 16 | |
private const val AFTER_PADDING_SKIP_BYTE_SIZE = 4 | |
private const val ONE_DIVISION_SIZE = 4 | |
} | |
} |
Author
shirobutton
commented
Feb 27, 2022
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment