安卓自定义弹幕实现

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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值