Created
March 7, 2021 06:19
-
-
Save HxBreak/e35839bc58a51fb09c3f04831a0ac881 to your computer and use it in GitHub Desktop.
Android DescentBasedImageSpan 基于字体底部位置显示的SpannableDrawable内容,设置行间距时图像位置不会显示异常
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
/** | |
* @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