Skip to content

Instantly share code, notes, and snippets.

@HxBreak
Created March 7, 2021 06:19
Show Gist options
  • Save HxBreak/e35839bc58a51fb09c3f04831a0ac881 to your computer and use it in GitHub Desktop.
Save HxBreak/e35839bc58a51fb09c3f04831a0ac881 to your computer and use it in GitHub Desktop.
Android DescentBasedImageSpan 基于字体底部位置显示的SpannableDrawable内容,设置行间距时图像位置不会显示异常
/**
* @author HxBreak
* @sample DescentBasedImageSpan(Context.getDrawable(R.mimap.ic_new), ViewUtils.dpToPx(4))
* An ImageSpannable BottomBased Line Descent Position And Horizontal Margin Supported
*/
package com.example.myapplication
import android.content.Context
import android.graphics.*
import android.graphics.Paint.FontMetricsInt
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.net.Uri
import android.text.TextPaint
import android.text.style.ReplacementSpan
import android.util.Log
import androidx.annotation.DrawableRes
import androidx.annotation.IntRange
import androidx.annotation.Px
import java.lang.ref.WeakReference
/**
* Span that replaces the text it's attached to with a [Drawable] that can be aligned with
* the bottom or with the baseline of the surrounding text. The drawable can be constructed from
* varied sources:
*
* * [Bitmap] - see [.ImageSpan] and
* [.ImageSpan]
*
* * [Drawable] - see [.ImageSpan]
* * resource id - see [.ImageSpan]
* * [Uri] - see [.ImageSpan]
*
* The default value for the vertical alignment is [android.text.style.DynamicDrawableSpan.ALIGN_BOTTOM]
*
*
* For example, an `ImagedSpan` can be used like this:
* <pre>
* SpannableString string = new SpannableString("Bottom: span.\nBaseline: span.");
* // using the default alignment: ALIGN_BOTTOM
* string.setSpan(new ImageSpan(this, R.mipmap.ic_launcher), 7, 8, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
* string.setSpan(new ImageSpan(this, R.mipmap.ic_launcher, DynamicDrawableSpan.ALIGN_BASELINE),
* 22, 23, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
</pre> *
* <img src="{@docRoot}reference/android/images/text/style/imagespan.png"></img>
* <figcaption>Text with `ImageSpan`s aligned bottom and baseline.</figcaption>
*/
class DescentBasedImageSpan : ReplacementSpan {
private var _mDrawable: Drawable? = null
private val mDrawable: Drawable
get() = _mDrawable!!
private var mContentUri: Uri? = null
@DrawableRes
private var mResourceId = 0
private var _mContext: Context? = null
private val mContext: Context
get() = _mContext!!
val paint = TextPaint()
private var _mDrawableRef: WeakReference<Drawable>? = null
/**
* Returns the source string that was saved during construction.
*
* @return the source string that was saved during construction
* @see .ImageSpan
* @see .ImageSpan
*/
var source: String? = null
private set
@Deprecated("Use {@link #ImageSpan(Context, Bitmap, int)} instead.")
constructor(b: Bitmap) : this(null, b)
/**
* Constructs an [android.text.style.ImageSpan] from a [Context], a [Bitmap] and a vertical
* alignment.
*
* @param context context used to create a drawable from {@param bitmap} based on
* the display metrics of the resources
* @param bitmap bitmap to be rendered
* @param verticalAlignment one of [android.text.style.DynamicDrawableSpan.ALIGN_BOTTOM] or
* [android.text.style.DynamicDrawableSpan.ALIGN_BASELINE]
*/
/**
* Constructs an [android.text.style.ImageSpan] from a [Context] and a [Bitmap] with the default
* alignment [android.text.style.DynamicDrawableSpan.ALIGN_BOTTOM]
*
* @param context context used to create a drawable from {@param bitmap} based on the display
* metrics of the resources
* @param bitmap bitmap to be rendered
*/
@JvmOverloads
constructor(context: Context?, bitmap: Bitmap) : super() {
_mContext = context
_mDrawable = if (context != null) BitmapDrawable(context.resources, bitmap) else BitmapDrawable(bitmap)
val width = mDrawable.getIntrinsicWidth()
val height = mDrawable.getIntrinsicHeight()
mDrawable.setBounds(0, 0, if (width > 0) width else 0, if (height > 0) height else 0)
}
/**
* Constructs an [android.text.style.ImageSpan] from a drawable and a vertical alignment.
*
* @param drawable drawable to be rendered
* @param verticalAlignment one of [android.text.style.DynamicDrawableSpan.ALIGN_BOTTOM] or
* [android.text.style.DynamicDrawableSpan.ALIGN_BASELINE]
*/
/**
* Constructs an [android.text.style.ImageSpan] from a drawable with the default
* alignment [android.text.style.DynamicDrawableSpan.ALIGN_BOTTOM].
*
* @param drawable drawable to be rendered
*/
var marginLeft = 0
var marginRight = 0
var horizontalMargin: Int
get() {
error("get is not allow")
}
set(value) {
marginLeft = value
marginRight = value
}
@JvmOverloads
constructor(drawable: Drawable, @Px horizontal: Int = 0) : super() {
_mDrawable = drawable
marginLeft = horizontal
marginRight = horizontal
}
/**
* Constructs an [android.text.style.ImageSpan] from a drawable, a source and a vertical alignment.
*
* @param drawable drawable to be rendered
* @param source drawable's uri source
* @param verticalAlignment one of [android.text.style.DynamicDrawableSpan.ALIGN_BOTTOM] or
* [android.text.style.DynamicDrawableSpan.ALIGN_BASELINE]
*/
/**
* Constructs an [android.text.style.ImageSpan] from a drawable and a source with the default
* alignment [android.text.style.DynamicDrawableSpan.ALIGN_BOTTOM]
*
* @param drawable drawable to be rendered
* @param source drawable's Uri source
*/
@JvmOverloads
constructor(drawable: Drawable, source: String) : super() {
_mDrawable = drawable
this.source = source
}
/**
* Constructs an [android.text.style.ImageSpan] from a [Context], a [Uri] and a vertical
* alignment. The Uri source can be retrieved via [.getSource]
*
* @param context context used to create a drawable from {@param bitmap} based on
* the display
* metrics of the resources
* @param uri [Uri] used to construct the drawable that will be rendered.
* @param verticalAlignment one of [android.text.style.DynamicDrawableSpan.ALIGN_BOTTOM] or
* [android.text.style.DynamicDrawableSpan.ALIGN_BASELINE]
*/
/**
* Constructs an [android.text.style.ImageSpan] from a [Context] and a [Uri] with the default
* alignment [android.text.style.DynamicDrawableSpan.ALIGN_BOTTOM]. The Uri source can be retrieved via
* [.getSource]
*
* @param context context used to create a drawable from {@param bitmap} based on the display
* metrics of the resources
* @param uri [Uri] used to construct the drawable that will be rendered
*/
@JvmOverloads
constructor(context: Context, uri: Uri) : super() {
_mContext = context
mContentUri = uri
source = uri.toString()
}
/**
* Constructs an [android.text.style.ImageSpan] from a [Context], a resource id and a vertical
* alignment.
*
* @param context context used to retrieve the drawable from resources
* @param resourceId drawable resource id based on which the drawable is retrieved.
* @param verticalAlignment one of [android.text.style.DynamicDrawableSpan.ALIGN_BOTTOM] or
* [DynamicDrawableSpan.ALIGN_BASELINE]
*/
/**
* Constructs an [android.text.style.ImageSpan] from a [Context] and a resource id with the default
* alignment [android.text.style.DynamicDrawableSpan.ALIGN_BOTTOM]
*
* @param context context used to retrieve the drawable from resources
* @param resourceId drawable resource id based on which the drawable is retrieved
*/
@JvmOverloads
constructor(context: Context, @DrawableRes resourceId: Int) : super() {
_mContext = context
mResourceId = resourceId
}
init {
paint.color = Color.RED
paint.strokeWidth = 2f
paint.style = Paint.Style.STROKE
}
fun getDrawable(): Drawable {
var drawable: Drawable? = null
if (_mDrawable != null) {
drawable = mDrawable
} else if (mContentUri != null) {
var bitmap: Bitmap? = null
try {
val `is` = mContext.contentResolver.openInputStream(
mContentUri!!)
bitmap = BitmapFactory.decodeStream(`is`)
drawable = BitmapDrawable(mContext.resources, bitmap)
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight())
`is`!!.close()
} catch (e: Exception) {
Log.e("ImageSpan", "Failed to loaded content $mContentUri", e)
}
} else {
try {
drawable = mContext.resources.getDrawable(mResourceId)
drawable!!.setBounds(0, 0, drawable.intrinsicWidth,
drawable.intrinsicHeight)
} catch (e: Exception) {
Log.e("ImageSpan", "Unable to find resource: $mResourceId")
}
}
return drawable!!
}
override fun getSize(paint: Paint, text: CharSequence?,
@IntRange(from = 0) start: Int, @IntRange(from = 0) end: Int,
fm: FontMetricsInt?): Int {
val d = getCachedDrawable()
val rect = d.bounds
if (fm != null) {
fm.ascent = -rect.bottom
fm.descent = 0
fm.top = fm.ascent
fm.bottom = 0
}
return rect.right + marginLeft + marginRight
}
private fun getCachedDrawable(): Drawable {
val wr = _mDrawableRef
var d: Drawable? = null
if (wr != null) {
d = wr.get()
}
if (d == null) {
d = getDrawable()
_mDrawableRef = WeakReference(d)
}
return d
}
val linePaint = Paint().apply {
color = Color.RED
strokeWidth = 2f
}
val topLinePaint = Paint().apply {
color = Color.BLACK
strokeWidth = 2f
}
val bottomLinePaint = Paint().apply {
color = Color.YELLOW
strokeWidth = 2f
}
companion object {
const val DEBUG = false
}
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, baseline: Int, bottom: Int, paint: Paint) {
val b = getCachedDrawable()
canvas.save()
val toY = baseline + paint.fontMetrics.descent - b.bounds.height()
if (DEBUG){
Log.e("HxBreak", String.format("${text?.subSequence(start, end)} x: %f, top: %d, baseline: %d, bottom: %d, a: %d, d: %d", x, top, baseline, bottom,
paint.fontMetricsInt.ascent, paint.fontMetricsInt.descent))
canvas.drawLine(0f, baseline.toFloat(), 400f, baseline.toFloat(), linePaint)
canvas.drawLine(0f, bottom.toFloat(), 1000f, bottom.toFloat(), bottomLinePaint)
canvas.drawLine(0f, toY.toFloat(), 800f, toY.toFloat(), linePaint)
canvas.drawLine(0f, top.toFloat(), 1000f, top.toFloat(), topLinePaint)
}
canvas.translate(x + marginLeft, toY)
if (DEBUG){
canvas.drawRect(Rect(0, 0, b.bounds.width(), b.bounds.height()), paint)
}
b.draw(canvas)
canvas.restore()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment