/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.views.text.frescosupport
import android.content.res.Resources
import android.graphics.BlendMode
import android.graphics.BlendModeColorFilter
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.widget.TextView
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder
import com.facebook.drawee.generic.GenericDraweeHierarchy
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder
import com.facebook.drawee.view.DraweeHolder
import com.facebook.imagepipeline.request.ImageRequest
import com.facebook.imagepipeline.request.ImageRequestBuilder
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.modules.fresco.ReactNetworkImageRequest
import com.facebook.react.uimanager.PixelUtil
import com.facebook.react.views.image.ImageResizeMode
import com.facebook.react.views.text.internal.span.TextInlineImageSpan
/**
* FrescoBasedTextInlineImageSpan is a span for Images that are inside . It computes its size
* based on the input size. When it is time to draw, it will use the Fresco framework to get the
* right Drawable and let that draw.
*
* Since Fresco needs to callback to the TextView that contains this, in the ViewManager, you must
* tell the Span about the TextView
*
* Note: It borrows code from DynamicDrawableSpan and if that code updates how it computes size or
* draws, we need to update this as well.
*/
internal class FrescoBasedReactTextInlineImageSpan(
resources: Resources,
height: Int,
width: Int,
private val tintColor: Int,
uri: Uri?,
private val headers: ReadableMap?,
private val draweeControllerBuilder: AbstractDraweeControllerBuilder<*, ImageRequest, *, *>,
private val callerContext: Any?,
private val resizeMode: String?,
) : TextInlineImageSpan() {
private var textView: TextView? = null
private val _uri: Uri = uri ?: Uri.EMPTY
private val _width: Int = PixelUtil.toPixelFromDIP(width.toDouble()).toInt()
private val _height: Int = PixelUtil.toPixelFromDIP(height.toDouble()).toInt()
private val draweeHolder: DraweeHolder =
DraweeHolder(GenericDraweeHierarchyBuilder.newInstance(resources).build())
override val width: Int
get() = _width
override val height: Int
get() = _height
override var drawable: Drawable? = null
private set
/**
* The ReactTextView that holds this ImageSpan is responsible for passing these methods on so that
* we can do proper lifetime management for Fresco
*/
override fun onDetachedFromWindow() {
draweeHolder.onDetach()
}
override fun onStartTemporaryDetach() {
draweeHolder.onDetach()
}
override fun onAttachedToWindow() {
draweeHolder.onAttach()
}
override fun onFinishTemporaryDetach() {
draweeHolder.onAttach()
}
override fun getSize(
paint: Paint,
text: CharSequence,
start: Int,
end: Int,
fm: Paint.FontMetricsInt?,
): Int {
// NOTE: This getSize code is copied from DynamicDrawableSpan and modified
// to not use a Drawable
fm?.let { fm ->
fm.ascent = -_height
fm.descent = 0
fm.top = fm.ascent
fm.bottom = 0
}
return _width
}
override fun setTextView(textView: TextView?) {
this.textView = textView
}
override fun draw(
canvas: Canvas,
text: CharSequence,
start: Int,
end: Int,
x: Float,
top: Int,
y: Int,
bottom: Int,
paint: Paint,
) {
if (drawable == null) {
val imageRequestBuilder = ImageRequestBuilder.newBuilderWithSource(_uri)
val imageRequest: ImageRequest =
ReactNetworkImageRequest.fromBuilderWithHeaders(imageRequestBuilder, headers)
draweeHolder.hierarchy.setActualImageScaleType(ImageResizeMode.toScaleType(resizeMode))
draweeControllerBuilder.reset()
draweeControllerBuilder.oldController = draweeHolder.controller
callerContext?.let { draweeControllerBuilder.setCallerContext(it) }
draweeControllerBuilder.setImageRequest(imageRequest)
val draweeController = draweeControllerBuilder.build()
draweeHolder.controller = draweeController
draweeControllerBuilder.reset()
checkNotNull(draweeHolder.topLevelDrawable).apply {
setBounds(0, 0, _width, _height)
if (tintColor != 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
colorFilter = BlendModeColorFilter(tintColor, BlendMode.SRC_IN)
} else {
colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)
}
}
callback = textView
drawable = this
}
}
// NOTE: This drawing code is copied from DynamicDrawableSpan
canvas.save()
// Align to center
val _drawable = checkNotNull(drawable)
val fontHeight = (paint.descent() - paint.ascent()).toInt()
val centerY = y + paint.descent().toInt() - fontHeight / 2
val transY = centerY - _drawable.bounds.height() / 2
canvas.translate(x, transY.toFloat())
_drawable.draw(canvas)
canvas.restore()
}
}