Skip to content

Instantly share code, notes, and snippets.

@ozodrukh
Created June 25, 2018 10:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ozodrukh/1985f5b7b73f9749dcee20554b11351e to your computer and use it in GitHub Desktop.
Save ozodrukh/1985f5b7b73f9749dcee20554b11351e to your computer and use it in GitHub Desktop.
Very simple Bitmap loading from Disk & Assets, it's taken from Picasso (c)
package com.ozodrukh.meet
import android.app.ActivityManager
import android.content.Context
import android.content.pm.ApplicationInfo
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapFactory.Options
import android.graphics.BitmapShader
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Picture
import android.graphics.Shader.TileMode.CLAMP
import android.media.ExifInterface.ORIENTATION_FLIP_HORIZONTAL
import android.media.ExifInterface.ORIENTATION_FLIP_VERTICAL
import android.media.ExifInterface.ORIENTATION_ROTATE_180
import android.media.ExifInterface.ORIENTATION_ROTATE_270
import android.media.ExifInterface.ORIENTATION_ROTATE_90
import android.media.ExifInterface.ORIENTATION_TRANSPOSE
import android.media.ExifInterface.ORIENTATION_TRANSVERSE
import android.net.Uri
import android.util.LruCache
import android.view.Gravity
import android.webkit.URLUtil
import io.reactivex.Single
import java.io.IOException
class BitmapHunter(context: Context) {
companion object {
const val ASSET_LEADING_PATH = "/android_asset/"
fun calculateMemoryCacheSize(context: Context): Int {
val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val largeHeap = context.getApplicationInfo().flags and ApplicationInfo.FLAG_LARGE_HEAP != 0
val memoryClass = if (largeHeap) am.getLargeMemoryClass() else am.getMemoryClass()
// Target ~15% of the available heap.
return (1024L * 1024L * memoryClass.toLong() / 7).toInt()
}
fun calculateInSampleSize(reqWidth: Int, reqHeight: Int, width: Int, height: Int,
options: BitmapFactory.Options, centerInside: Boolean) {
var sampleSize = 1
if (height > reqHeight || width > reqWidth) {
val heightRatio: Int
val widthRatio: Int
if (reqHeight == 0) {
sampleSize = Math.floor((width.toFloat() / reqWidth.toFloat()).toDouble()).toInt()
} else if (reqWidth == 0) {
sampleSize = Math.floor((height.toFloat() / reqHeight.toFloat()).toDouble()).toInt()
} else {
heightRatio = Math.floor((height.toFloat() / reqHeight.toFloat()).toDouble()).toInt()
widthRatio = Math.floor((width.toFloat() / reqWidth.toFloat()).toDouble()).toInt()
sampleSize = if (centerInside)
Math.max(heightRatio, widthRatio)
else
Math.min(heightRatio, widthRatio)
}
}
options.inSampleSize = sampleSize
options.inJustDecodeBounds = false
}
fun transformResult(data: BitmapRequestOptions, result: Bitmap, exifOrientation: Int): Bitmap {
var result = result
val inWidth = result.width
val inHeight = result.height
val onlyScaleDown = data.onlyScaleDown
var drawX = 0
var drawY = 0
var drawWidth = inWidth
var drawHeight = inHeight
val matrix = Matrix()
if (data.hasSize() || exifOrientation != 0) {
var targetWidth = data.targetWidth
var targetHeight = data.targetHeight
// EXIf interpretation should be done before cropping in case the dimensions need to
// be recalculated
if (exifOrientation != 0) {
val exifRotation = getExifRotation(exifOrientation)
val exifTranslation = getExifTranslation(exifOrientation)
if (exifRotation != 0) {
matrix.preRotate(exifRotation * 1f)
if (exifRotation == 90 || exifRotation == 270) {
// Recalculate dimensions after exif rotation
val tmpHeight = targetHeight
targetHeight = targetWidth
targetWidth = tmpHeight
}
}
if (exifTranslation != 1) {
matrix.postScale(exifTranslation * 1f, 1f)
}
}
if (data.centerCrop) {
// Keep aspect ratio if one dimension is set to 0
val widthRatio =
if (targetWidth != 0) targetWidth / inWidth.toFloat() else targetHeight / inHeight.toFloat()
val heightRatio =
if (targetHeight != 0) targetHeight / inHeight.toFloat() else targetWidth / inWidth.toFloat()
val scaleX: Float
val scaleY: Float
if (widthRatio > heightRatio) {
val newSize = Math.ceil((inHeight * (heightRatio / widthRatio)).toDouble()).toInt()
if (data.centerCropGravity and Gravity.TOP == Gravity.TOP) {
drawY = 0
} else if (data.centerCropGravity and Gravity.BOTTOM == Gravity.BOTTOM) {
drawY = inHeight - newSize
} else {
drawY = (inHeight - newSize) / 2
}
drawHeight = newSize
scaleX = widthRatio
scaleY = targetHeight / drawHeight.toFloat()
} else if (widthRatio < heightRatio) {
val newSize = Math.ceil((inWidth * (widthRatio / heightRatio)).toDouble()).toInt()
if (data.centerCropGravity and Gravity.LEFT == Gravity.LEFT) {
drawX = 0
} else if (data.centerCropGravity and Gravity.RIGHT == Gravity.RIGHT) {
drawX = inWidth - newSize
} else {
drawX = (inWidth - newSize) / 2
}
drawWidth = newSize
scaleX = targetWidth / drawWidth.toFloat()
scaleY = heightRatio
} else {
drawX = 0
drawWidth = inWidth
scaleY = heightRatio
scaleX = scaleY
}
if (shouldResize(onlyScaleDown, inWidth, inHeight, targetWidth, targetHeight)) {
matrix.preScale(scaleX, scaleY)
}
} else if (data.centerInside) {
// Keep aspect ratio if one dimension is set to 0
val widthRatio =
if (targetWidth != 0) targetWidth / inWidth.toFloat() else targetHeight / inHeight.toFloat()
val heightRatio =
if (targetHeight != 0) targetHeight / inHeight.toFloat() else targetWidth / inWidth.toFloat()
val scale = if (widthRatio < heightRatio) widthRatio else heightRatio
if (shouldResize(onlyScaleDown, inWidth, inHeight, targetWidth, targetHeight)) {
matrix.preScale(scale, scale)
}
} else if ((targetWidth != 0 || targetHeight != 0) //
&& (targetWidth != inWidth || targetHeight != inHeight)) {
// If an explicit target size has been specified and they do not match the results bounds,
// pre-scale the existing matrix appropriately.
// Keep aspect ratio if one dimension is set to 0.
val sx =
if (targetWidth != 0) targetWidth / inWidth.toFloat() else targetHeight / inHeight.toFloat()
val sy =
if (targetHeight != 0) targetHeight / inHeight.toFloat() else targetWidth / inWidth.toFloat()
if (shouldResize(onlyScaleDown, inWidth, inHeight, targetWidth, targetHeight)) {
matrix.preScale(sx, sy)
}
}
}
val newResult = Bitmap.createBitmap(result, drawX, drawY, drawWidth, drawHeight, matrix, true)
if (newResult != result) {
result.recycle()
result = newResult
}
return result
}
private fun shouldResize(onlyScaleDown: Boolean, inWidth: Int, inHeight: Int,
targetWidth: Int, targetHeight: Int): Boolean {
return (!onlyScaleDown || targetWidth != 0 && inWidth > targetWidth
|| targetHeight != 0 && inHeight > targetHeight)
}
fun getExifRotation(orientation: Int): Int {
val rotation: Int
when (orientation) {
ORIENTATION_ROTATE_90, ORIENTATION_TRANSPOSE -> rotation = 90
ORIENTATION_ROTATE_180, ORIENTATION_FLIP_VERTICAL -> rotation = 180
ORIENTATION_ROTATE_270, ORIENTATION_TRANSVERSE -> rotation = 270
else -> rotation = 0
}
return rotation
}
fun getExifTranslation(orientation: Int): Int {
val translation: Int
when (orientation) {
ORIENTATION_FLIP_HORIZONTAL, ORIENTATION_FLIP_VERTICAL, ORIENTATION_TRANSPOSE, ORIENTATION_TRANSVERSE -> translation =
-1
else -> translation = 1
}
return translation
}
}
private val context = context.applicationContext
private val bitmapsCache = PlatformLruCache<String>(calculateMemoryCacheSize(context))
private val picturesCache = LruCache<String, Picture>(100)
fun getCachedBitmap(file: Uri, options: BitmapRequestOptions): Bitmap? {
return bitmapsCache[options.getKey(file)]
}
fun getCachedPicture(file: Uri, options: PictureRequest): Picture? {
return picturesCache[options.getKey(file)]
}
fun clearCache() {
bitmapsCache.clear()
picturesCache.evictAll()
}
fun getPicture(file: Uri, bitmap: Bitmap, options: PictureRequest): Picture {
val cachedPicture = Picture()
val width = if (options.size == 0) bitmap.width else options.size
val height = if (options.size == 0) bitmap.height else options.size
val canvas = cachedPicture.beginRecording(width, height)
if (options.circle) {
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.shader = BitmapShader(bitmap, CLAMP, CLAMP)
val size = if (options.size == 0) bitmap.width else options.size
canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
} else {
canvas.drawBitmap(bitmap, 0f, 0f, null)
}
cachedPicture.endRecording()
picturesCache.put(options.getKey(file), cachedPicture)
return cachedPicture
}
private fun getBitmapByOptions(file: Uri, options: Options): Bitmap? {
if (URLUtil.isAssetUrl(file.toString())) {
val path = file.path.removePrefix(ASSET_LEADING_PATH)
return BitmapFactory.decodeStream(context.assets.open(path), null, options)
} else if (URLUtil.isFileUrl(file.toString())) {
return BitmapFactory.decodeFile(file.path, options)
} else {
throw RuntimeException("Unsupported uri=$file")
}
}
fun getBitmap(file: Uri, options: BitmapRequestOptions): Single<Bitmap> {
return Single
.fromCallable {
val opts = BitmapFactory.Options()
opts.inPreferredConfig = options.config
opts.inJustDecodeBounds = true
getBitmapByOptions(file, opts)
return@fromCallable opts
}
.flatMap { opts ->
calculateInSampleSize(options.targetWidth, options.targetHeight,
opts.outWidth, opts.outHeight,
opts,
options.centerInside)
val decodedImage = getBitmapByOptions(file, opts)
if (decodedImage == null) {
Single.error(IOException("Couldn't decode stream"))
} else {
Single.just(transformResult(options, decodedImage, 0))
}
}
.doOnSuccess {
bitmapsCache[options.getKey(file)] = it
}
}
class PictureRequest(
val circle: Boolean = false,
val size: Int = 0,
val bitmapOptions: BitmapRequestOptions = BitmapRequestOptions(size, size)) {
fun getKey(file: Uri): String {
return "$file;circle:$circle;size:$size;"
}
}
class BitmapRequestOptions(
val targetWidth: Int,
val targetHeight: Int,
val config: Bitmap.Config = Bitmap.Config.RGB_565,
val centerInside: Boolean = false,
val centerCrop: Boolean = false,
val centerCropGravity: Int = Gravity.CENTER,
val onlyScaleDown: Boolean = false) {
constructor(bitmapOptions: BitmapRequestOptions,
targetWidth: Int = bitmapOptions.targetHeight,
targetHeight: Int = bitmapOptions.targetWidth,
config: Bitmap.Config = bitmapOptions.config,
centerInside: Boolean = bitmapOptions.centerInside,
centerCrop: Boolean = bitmapOptions.centerCrop,
centerCropGravity: Int = bitmapOptions.centerCropGravity,
onlyScaleDown: Boolean = bitmapOptions.onlyScaleDown)
: this(targetWidth, targetHeight,
config, centerInside, centerCrop, centerCropGravity, onlyScaleDown)
init {
if (centerCrop && centerInside) {
throw IllegalArgumentException("Both centerCrop & centerInside features couldn't be turned on")
}
}
fun hasSize(): Boolean {
return targetHeight != 0 || targetWidth != 0
}
fun getKey(file: Uri): String {
val builder = StringBuilder()
builder.append(file).append(";")
if (hasSize()) {
builder.append("resize:").append(targetWidth).append('x').append(targetHeight)
.append(";")
}
if (centerCrop) {
builder.append("centerCrop:").append(centerCropGravity).append(";")
} else if (centerInside) {
builder.append("centerInside:").append(";")
}
return builder.toString()
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment