class BarrageTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
companion object {
const val HD = 0
const val CIRCLE = 1
const val RECTANGLE = 2
const val STAR = 3
const val TRIANGLE = 4
const val VERTICAL = 0
const val HORIZONTAL = 1
}
private var mShapeMargin = 20f //每个图形的间隔
private var mShapeSize = 10f //控制图形大小
private var mCurPos = FloatArray(2)
private var mTextStyle = Typeface.NORMAL
private var mText = ""
private var mTextColor = Color.RED
private var mTextSize = 100f
private var mTextType = HD
private var mOrientation = VERTICAL
private var mFlickerTime = FlashlightConstant.FLICKER_DEFAULT_TIME
private var mMoveTime = FlashlightConstant.BARRAGE_DEFAULT_TIME
//绘制字体
private val mTextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
private val mFontPath = Path()
private val mShapePath = Path()
private var mPathMeasure = PathMeasure()
private val mFontBounds = Rect()
private var mMarqueeAnimator: ObjectAnimator? = null
private var mFlickerAnimator: ValueAnimator? = null
init {
val obtain = context.obtainStyledAttributes(attrs, R.styleable.BarrageTextView)
mText = obtain.getString(R.styleable.BarrageTextView_barrageText) ?: mText
mTextColor = obtain.getColor(R.styleable.BarrageTextView_barrageTextColor, mTextColor)
mTextSize = obtain.getDimension(R.styleable.BarrageTextView_barrageTextSize, mTextSize)
mTextStyle = obtain.getInt(R.styleable.BarrageTextView_barrageTextStyle, mTextStyle)
mTextType = obtain.getInt(R.styleable.BarrageTextView_barrageTextType, mTextType)
mOrientation = obtain.getInt(R.styleable.BarrageTextView_barrageTextOrientation, mOrientation)
mFlickerTime = obtain.getInt(R.styleable.BarrageTextView_barrageFlickerTime, mFlickerTime.toInt()).toLong()
mMoveTime = obtain.getInt(R.styleable.BarrageTextView_barrageMoveTime, mMoveTime.toInt()).toLong()
obtain.recycle()
initPaint()
}
private fun initPaint() {
mTextPaint.apply {
color = mTextColor
textSize = mTextSize
typeface = Typeface.create(Typeface.DEFAULT, mTextStyle)
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val ascent = -mTextPaint.fontMetrics.ascent
mTextPaint.getTextPath(mText, 0, mText.length, 0f, ascent, mFontPath)
mTextPaint.getTextBounds(mText, 0, mText.length, mFontBounds)
if (mOrientation == VERTICAL) {
setMeasuredDimension(mFontBounds.height() + mTextPaint.descent().toInt(), mFontBounds.width() + mTextPaint.descent().toInt())
} else {
setMeasuredDimension(mFontBounds.width(), mFontBounds.height() + mTextPaint.descent().toInt())
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (canvas == null) return
// 改变Canvas坐标系,使文字在竖直方向绘制
if (mOrientation == VERTICAL) {
canvas.translate(mFontBounds.height() + mTextPaint.descent(),0f)
canvas.rotate(90f)
}
mPathMeasure.setPath(mFontPath, false)
if (mTextType == HD) {
canvas.drawPath(mFontPath, mTextPaint)
}
//设置完mPathMeasure.setPath(mFontPath, false)一定要重置mFontPath不然会一直添加, mPathMeasure.length就会翻倍 就会造成ANR
mFontPath.reset()
var hasMore = true
//根据进度获取路径
while (hasMore) {
val length = mPathMeasure.length
mPathMeasure.getSegment(0f, length, mFontPath, true)
val number = (length / mShapeMargin).toInt()
for (i in 0 until number) {
val distance = (length / number) * i
mPathMeasure.getPosTan(distance, mCurPos, null)
mShapePath.reset()
drawTextShape(canvas)
}
hasMore = mPathMeasure.nextContour()
}
}
private fun drawTextShape(canvas: Canvas) {
when(mTextType) {
CIRCLE -> canvas.drawCircle(mCurPos[0], mCurPos[1], mShapeSize, mTextPaint)
RECTANGLE -> {
val left = mCurPos[0] - mShapeSize
val top = mCurPos[1] - mShapeSize
val right = mCurPos[0] + mShapeSize
val bottom = mCurPos[1] + mShapeSize
canvas.drawRect(RectF(left, top, right, bottom), mTextPaint)
}
STAR -> drawStarShape(canvas)
TRIANGLE -> drawTriangleShape(canvas)
}
}
private fun drawStarShape(canvas: Canvas) {
//360度除以5个角, 比如5个角 每个角72度,利用cos 、sin计算位置 1度=π/180 以顶角尖作为坐标轴原点
val outRadian = Math.PI / 180.0 * (90.0 - 360.0 / 5 / 2) //右角尖与x轴的弧度
val pRadian = (1.0 - 0.5) * outRadian //钝角点与x轴的弧度
val pRightRadian = 0.5 * outRadian //钝角点到原点与右角尖的弧度
val centerLength = mShapeSize * sin(Math.PI / 5) //右角尖到顶角间的中心点到原点的距离
val pLength = centerLength / cos(pRightRadian) //钝角点到原点距离
val pX = pLength * sin(pRadian)
val pY = pLength * cos(pRadian)
for (i in 0..4) {
val angle = Math.PI / 180 * 360 / 5 * i //每次旋转坐标轴角度 angle== π/180 * (360 / 5)
val sinA = Math.sin(angle)
val cosA = Math.cos(angle)
val rX2 = (mShapeSize * sinA).toFloat()
val rY2 = (-mShapeSize * cosA + mShapeSize).toFloat()
val pX2 = (pX * cosA - (pY - mShapeSize) * sinA).toFloat()
val pY2 = ((pY - mShapeSize) * cosA + pX * sinA + mShapeSize).toFloat()
mShapePath.lineTo(rX2, rY2)
mShapePath.lineTo(pX2, pY2)
}
mShapePath.close()
val matrix = Matrix()
//坐标系平移
matrix.setTranslate(mCurPos[0], mCurPos[1])
mShapePath.transform(matrix)
canvas.drawPath(mShapePath, mTextPaint)
}
private fun drawTriangleShape(canvas: Canvas) {
val point2X = mCurPos[0] - (mShapeSize * sqrt(3.0) / 2).toFloat() // 左下角
val point2Y = mCurPos[1] + (mShapeSize / 2) // 左下角
val point3X = mCurPos[0] + (mShapeSize * sqrt(3.0) / 2).toFloat() // 右下角
val point3Y = mCurPos[1] + (mShapeSize / 2) // 右下角
mShapePath.moveTo(mCurPos[0], mCurPos[1] - mShapeSize)
mShapePath.lineTo(point2X, point2Y)
mShapePath.lineTo(point3X, point3Y)
mShapePath.close()
canvas.drawPath(mShapePath, mTextPaint)
}
fun startMarquee(time: Long = mMoveTime) {
if (!isVisible) return
mMoveTime = time
val animatorName = if (mOrientation == VERTICAL) "translationY" else "translationX"
post {
val start = if (mOrientation == VERTICAL) ScreenUtils.getScreenHeight() else ScreenUtils.getScreenWidth()
val end = if (mOrientation == VERTICAL) -(mFontBounds.width() + mTextPaint.descent()) else - max(mFontBounds.width(), ScreenUtils.getScreenWidth()).toFloat()
mMarqueeAnimator = ObjectAnimator.ofFloat(this, animatorName, start.toFloat(), end)
mMarqueeAnimator?.duration = mMoveTime
mMarqueeAnimator?.interpolator = LinearInterpolator()
mMarqueeAnimator?.repeatCount = ValueAnimator.INFINITE
mMarqueeAnimator?.start()
}
}
fun startFlicker(time: Long = mFlickerTime) {
if (!isVisible) return
mFlickerTime = time
post {
mFlickerAnimator = ValueAnimator.ofFloat(1f, 0f, 1f)
mFlickerAnimator?.interpolator = LinearInterpolator()
mFlickerAnimator?.duration = mFlickerTime
mFlickerAnimator?.repeatCount = ValueAnimator.INFINITE
mFlickerAnimator?.addUpdateListener {
val value = it.animatedValue as Float
alpha = value
}
mFlickerAnimator?.start()
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
mMarqueeAnimator?.cancel()
mMarqueeAnimator = null
mFlickerAnimator?.cancel()
mFlickerAnimator?.removeAllUpdateListeners()
mFlickerAnimator = null
}
fun setTextColor(color: Int) {
this.mTextColor = color
mTextPaint.color = mTextColor
invalidate()
}
fun setTextSize(size: Float) {
this.mTextSize = ConvertUtils.dp2px(size).toFloat()
mTextPaint.textSize = mTextSize
//动态调整间距 不然字体小的话看不清字
mShapeMargin = 10 + (mShapeMargin / 100f * size - 10)
mShapeSize = 5 + (mShapeSize / 100f * size - 5)
invalidate()
requestLayout()
}
fun setTextType(type: Int) {
this.mTextType = type
invalidate()
}
fun setText(text: String?) {
if (text.isNullOrEmpty()) return
this.mText = text
requestLayout()
invalidate()
}
fun getText() = mText
}
attrs文件
<declare-styleable name="BarrageTextView">
<attr name="barrageText" format="string"/>
<attr name="barrageTextSize" format="dimension|reference"/>
<attr name="barrageTextColor" format="color|reference"/>
<attr name="barrageTextOrientation">
<enum name="vertical" value="0" />
<enum name="horizontal" value="1" />
</attr>
<attr name="barrageTextStyle">
<enum name="normal" value="0" />
<enum name="bold" value="1" />
<enum name="italic" value="2" />
</attr>
<attr name="barrageTextType" format="integer">
<enum name="hd" value="0" />
<enum name="circle" value="1" />
<enum name="rectangle" value="2" />
<enum name="star" value="3" />
<enum name="triangle" value="4" />
</attr>
<attr name="barrageFlickerTime" format="integer"/> <!--闪烁时间-->
<attr name="barrageMoveTime" format="integer"/> <!--移动时间-->
</declare-styleable>