Android 高度自定義的粒子框架(支持普通View和surfaceView)

序言

當(dāng)前Android 主流的系統(tǒng)中粒子的實(shí)現(xiàn)方式大致可以分為兩類三種

1. 繼承普通的View善涨,主線程刷新繪制

2. 使用surfaceView,子線程刷新繪制

3. 使用surfaceView+openGl啃奴,子線程刷新繪制

因?yàn)榈谌N方式不是很常用潭陪,這里使用前兩種方式實(shí)現(xiàn)粒子

效果圖(視頻轉(zhuǎn)GIf有點(diǎn)卡)

image

實(shí)現(xiàn)功能

1. 可以自定義粒子刷新的頻率,每次刷新的數(shù)量

2. 粒子自己維護(hù)自己的生命周期

3. 可以預(yù)先自己定義的粒子數(shù)量加載粒子 避免一加載就是滿屏粒子的尷尬

4. 復(fù)用消亡粒子

5. 實(shí)現(xiàn)粒子的高度自定義 避免過度封裝

理論概述

兩種實(shí)現(xiàn)方式 對(duì)粒子的處理大致相同 不同的是繪制的位置區(qū)別

這里以第一種方式為例 畫出粒子的UML框架:

image

如上圖所示

ParticleView繼承Runnable 使用handler定時(shí)刷新最蕾,每次刷新進(jìn)行以下一個(gè)步驟

1. 新增粒子 從cacheItems集合中獲取緩存粒子 如果沒有則調(diào)用adapter進(jìn)行創(chuàng)建

2. 調(diào)用transForms 移動(dòng)粒子并將消亡粒子移除繪制集合中

3. 調(diào)用drawItems方法繪制所有的粒子

使用

這里因?yàn)槭歉叨荣Y質(zhì)粒子依溯,所以如果我們想要繪制粒子需要首先繼承BaseItem接口定義自己的粒子類:

然后實(shí)現(xiàn)適配器ParticleAdapter

如:

  particle.setAdapter(object : ParticleAdapter() {

            override fun newItem(parentWidth: Int, parentHeight: Int): BaseItem {

   //返回自己實(shí)現(xiàn)的粒子

                return BubbleItem(parentWidth, parentHeight, this@MainActivity)

            }

            override fun preCreateCount(): Int {

                return 50

            }

        })

        particle.setIncreaseParticleInterval(100)

        particle.setRenderTime(16)

        particle.setIncreaseParticleCount(1)

我這里實(shí)現(xiàn)的粒子如下:BubbleItem.kotlin

class BubbleItem(private val parentWidth: Int, private val parentHeight: Int,context: Context) :

    BaseItem {

    companion object {

        const val STATE_LIVE = 1

        const val STATE_DIE = 0

    }

    private val baseBubbleRadius = ScreenUtil.dip2px(context,2f)

    private val intervalBubbleRadius = ScreenUtil.dip2px(context,3f)

    //起點(diǎn)

    private var origX: Float = 0f

    private var origY: Float = parentHeight.toFloat()

    //終點(diǎn)

    private var desY: Float = 0f

    //當(dāng)前的位置

    private var curX = 0f

    private var curY = 0f

    //每次刷新 在Y軸的偏移量

    private var speedY = ScreenUtil.dip2px(context,2f)

    private val baseSpeedX =ScreenUtil.dip2px(context,0.5f)

    //每次刷新 在X軸的偏移量

    private var speedX = 0f

    var radius = 20f

    //透明的距離

    private var alphaDis = 0f

    var state = STATE_DIE

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)

    private var drawBitmap: Bitmap? = null

    private var resRect: Rect? = null

    init {

        paint.style = Paint.Style.FILL_AND_STROKE

        paint.color = Color.BLUE

    }

    /**

    * 初始化 隨機(jī)生成氣泡的出生地點(diǎn)

    */

    override fun init(context: Context) {

        //獲取氣泡的bitmap

        if (drawBitmap == null) {

            drawBitmap = BitmapFactory.decodeResource(context.resources, R.drawable.bubble)

            resRect = Rect(0, 0, drawBitmap?.width ?: 0, drawBitmap?.height ?: 0)

        }

        origX = Random.nextInt(100, parentWidth - 100).toFloat()

        desY = 2 * parentHeight / 3 - Random.nextInt(0, parentHeight / 2).toFloat()

        alphaDis = (origY - desY) * 0.2f

        radius = Random.nextFloat() * intervalBubbleRadius + baseBubbleRadius

        speedX = baseSpeedX * Random.nextFloat() * if (Random.nextBoolean()) {

            1

        } else {

            -1

        }

        curX = origX

        curY = origY

        state = STATE_LIVE

        //在邊界處的粒子 沒有橫向速度

        if (curX <= 200 || curX > (parentWidth - 200)) {

            speedX = 0f

        }

        paint.alpha = 255

    }

    override fun preInit(context: Context) {

        //起點(diǎn)的X軸坐標(biāo)

        init(context)

        curY = desY + max((origY - desY) * Random.nextFloat(), 0f)

    }

    override fun move(): Boolean {

        curY -= speedY

        curX += speedX

        val diff = curY - desY

        if (diff <= alphaDis) {

            if (diff <= alphaDis * 0.4 && diff >=0.3 * alphaDis) {

                paint.alpha = 255

            } else {

                //開始透明

                paint.alpha = (255 * diff / alphaDis + 0.5f).toInt()

            }

        }

        if (curY < desY) {

            state = STATE_DIE

            return false

        }

        if (curX <= 20 || curX >= parentWidth - 20) {

            state = STATE_DIE

            return false

        }

        return true

    }

    override fun reset() {

    }

    override fun draw(canvas: Canvas) {

        drawBitmap?.apply {

            if (!isRecycled) {

                canvas.drawBitmap(this, resRect, RectF(curX - radius, curY - radius, curX + radius, curY + radius), paint)

            }

        }

    }

    override fun isLive(): Boolean {

        return state == STATE_LIVE

    }

    override fun destroy() {

        drawBitmap?.recycle()

        drawBitmap = null

    }

}

使用普通View 主線程刷新繪制

普通的View繪制就是 使用handler主線程繪制刷新實(shí)現(xiàn)如下:

class ParticleView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet),

    BaseParticle, Runnable {

    private var particleAdapter: ParticleAdapter? = null

    private var isAutoPlay = true

    private var intervalTime = 10 * Constant.RENDER_TIME

    private var renderTime = Constant.RENDER_TIME

    private var increaseParticleCount = 1

    private var childTotal = Int.MAX_VALUE

    private var temTime = -1

    //緩存的view

    private var cacheItems = LinkedList<BaseItem>()

    //要繪制的所有View

    private var drawItems = ArrayList<BaseItem>()

    private var renderHandler: Handler = Handler()

    private var isInit: Boolean = false

    override fun preCreate() {

        repeat(particleAdapter?.preCreateCount() ?: 0) {

            val newItem = particleAdapter!!.newItem(measuredWidth, measuredHeight)

            newItem.preInit(context)

            drawItems.add(newItem)

        }

    }

    override fun getItem(): BaseItem {

        val newItem = if (cacheItems.size > 0) {

            cacheItems.removeFirst()

        } else {

            particleAdapter!!.newItem(measuredWidth, measuredHeight)

        }

        newItem.init(context)

        return newItem

    }

    override fun drawItems(canvas: Canvas) {

        if (drawItems.size < childTotal) {

            if (temTime == -1) {

                temTime = ((intervalTime / renderTime.toFloat() + 0.5).toInt())

            } else if (temTime == 0) {

                repeat(increaseParticleCount) {

                    drawItems.add(getItem())

                }

            }

            temTime--

        }

        drawItems.forEach {

            it.draw(canvas)

        }

    }

    override fun transForms() {

        val iterator = drawItems.iterator()

        while (iterator.hasNext()) {

            val next = iterator.next()

            val isLive = next.move()

            if (!isLive) {

                iterator.remove()

                cacheItems.add(next)

            }

        }

    }

    override fun destroyAllView() {

        drawItems.forEach {

            it.destroy()

        }

        cacheItems.forEach {

            it.destroy()

        }

    }

    override fun startAnimation() {

        renderHandler.removeCallbacks(this)

        renderHandler.post(this)

    }

    override fun stopAnimation() {

        renderHandler.removeCallbacks(this)

    }

    override fun setAdapter(adapter: ParticleAdapter) {

        particleAdapter = adapter

        if (particleAdapter!!.maxParticleCount() <= 0) {

            return

        }

        childTotal = particleAdapter!!.maxParticleCount()

    }

    override fun setRenderTime(renderTime: Long) {

        if (intervalTime < renderTime || renderTime < 0) {

            return

        }

        this.renderTime = renderTime

    }

    override fun setIncreaseParticleInterval(intervalTime: Long) {

        if (intervalTime < renderTime || renderTime < 0) {

            return

        }

        this.intervalTime = intervalTime

    }

    override fun setIncreaseParticleCount(count: Int) {

        if (count <= 0) {

            return

        }

        increaseParticleCount = count

    }

    override fun setIsAutoPlay(isAuto: Boolean) {

        isAutoPlay = isAuto

    }

    /**

    * 設(shè)置大小則按照設(shè)置的大小計(jì)算 否則按照屏幕的寬高來計(jì)算

    */

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

        //獲取屏幕寬高

        val screenWidth = ScreenUtil.getScreenWidth(context)

        val screenHeight = ScreenUtil.getScreenRealHeight(context)

        setMeasuredDimension(

            getDefaultSize(screenWidth, widthMeasureSpec),

            getDefaultSize(screenHeight, screenHeight)

        )

    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {

        super.onLayout(changed, left, top, right, bottom)

        if (!isInit) {

            isInit = true

            preCreate()

        }

    }

    override fun onDraw(canvas: Canvas?) {

        if (visibility != VISIBLE) {

            return

        }

        if (canvas == null) {

            renderHandler.removeCallbacks(this)

            return

        }

        drawItems(canvas)

    }

    override fun run() {

        transForms()

        invalidate()

        renderHandler.postDelayed(this, renderTime)

    }

    override fun setVisibility(visibility: Int) {

        super.setVisibility(visibility)

        if (isAutoPlay) {

            if (visibility == VISIBLE) {

                startAnimation()

            } else {

                renderHandler.removeCallbacksAndMessages(null)

            }

        }

    }

    override fun onDetachedFromWindow() {

        super.onDetachedFromWindow()

        renderHandler.removeCallbacks(this)

    }

    override fun onAttachedToWindow() {

        super.onAttachedToWindow()

        renderHandler.post(this)

    }

}

使用surfaceView,子線程刷新繪制

實(shí)現(xiàn)如下:

class ParticleSurfaceView(context: Context, attributeSet: AttributeSet) :

    SurfaceView(context, attributeSet), BaseParticle, SurfaceHolder.Callback, Runnable {

    private var particleAdapter: ParticleAdapter? = null

    private var isAutoPlay = true

    private var intervalTime = 10 * Constant.RENDER_TIME

    private var renderTime = Constant.RENDER_TIME

    private var increaseParticleCount = 1

    private var childTotal = Int.MAX_VALUE

    private var temTime = -1

    private val surfaceHolder: SurfaceHolder = holder

    private lateinit var handlerThread: HandlerThread

    private lateinit var renderHandler: Handler

    //緩存的view

    private var cacheItems = LinkedList<BaseItem>()

    //要繪制的所有View

    private var drawItems = ArrayList<BaseItem>()

    init {

        surfaceHolder.setKeepScreenOn(true)

        surfaceHolder.addCallback(this)

        isFocusable = true

        isFocusableInTouchMode = true

        setZOrderOnTop(true)

        //設(shè)置背景為透明色

        surfaceHolder.setFormat(PixelFormat.TRANSPARENT)

    }

    override fun preCreate() {

        repeat(particleAdapter?.preCreateCount() ?: 0) {

            val newItem = particleAdapter!!.newItem(measuredWidth, measuredHeight)

            newItem.preInit(context)

            drawItems.add(newItem)

        }

    }

    override fun getItem(): BaseItem {

        val newItem = if (cacheItems.size > 0) {

            cacheItems.removeFirst()

        } else {

            particleAdapter!!.newItem(measuredWidth, measuredHeight)

        }

        newItem.init(context)

        return newItem

    }

    override fun drawItems(canvas: Canvas) {

        if (drawItems.size < childTotal) {

            if (temTime == -1) {

                temTime = ((intervalTime / renderTime.toFloat() + 0.5).toInt())

            } else if (temTime == 0) {

                repeat(increaseParticleCount) {

                    drawItems.add(getItem())

                }

            }

            temTime--

        }

        drawItems.forEach {

            it.draw(canvas)

        }

    }

    override fun transForms() {

        val iterator = drawItems.iterator()

        while (iterator.hasNext()) {

            val next = iterator.next()

            val isLive = next.move()

            if (!isLive) {

                iterator.remove()

                cacheItems.add(next)

            }

        }

    }

    override fun destroyAllView() {

        drawItems.forEach {

            it.destroy()

        }

        cacheItems.forEach {

            it.destroy()

        }

    }

    override fun startAnimation() {

        renderHandler.removeCallbacks(this)

        renderHandler.post(this)

    }

    override fun stopAnimation() {

        renderHandler.removeCallbacks(this)

    }

    override fun setAdapter(adapter: ParticleAdapter) {

        particleAdapter = adapter

        if (particleAdapter!!.maxParticleCount() <= 0) {

            return

        }

        childTotal = particleAdapter!!.maxParticleCount()

    }

    override fun setRenderTime(renderTime: Long) {

        if (intervalTime < renderTime || renderTime < 0) {

            return

        }

        this.renderTime = renderTime

    }

    override fun setIncreaseParticleInterval(intervalTime: Long) {

        if (intervalTime < renderTime || renderTime < 0) {

            return

        }

        this.intervalTime = intervalTime

    }

    override fun setIncreaseParticleCount(count: Int) {

        if (count <= 0) {

            return

        }

        increaseParticleCount = count

    }

    override fun setIsAutoPlay(isAuto: Boolean) {

        isAutoPlay = isAuto

    }

    override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {

    }

    override fun surfaceDestroyed(holder: SurfaceHolder?) {

        renderHandler.removeCallbacks(this)

        destroyAllView()

    }

    override fun surfaceCreated(holder: SurfaceHolder?) {

        if (particleAdapter == null) {

            throw NullPointerException("particleAdapter must not be null")

        }

        handlerThread = HandlerThread("BaseSurfaceView")

        handlerThread.start()

        renderHandler = Handler(handlerThread.looper)

        preCreate()

        if (isAutoPlay) {

            renderHandler.post(this)

        }

    }

    override fun run() {

        transForms()

        val lockCanvas = surfaceHolder.lockCanvas(null)

        lockCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)

        drawItems(lockCanvas)

        surfaceHolder.unlockCanvasAndPost(lockCanvas)

        renderHandler.postDelayed(this, renderTime)

    }

}

項(xiàng)目地址:項(xiàng)目地址傳送門 點(diǎn)我 點(diǎn)我 點(diǎn)我!!!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瘟则,一起剝皮案震驚了整個(gè)濱河市黎炉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌醋拧,老刑警劉巖慷嗜,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件淀弹,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡庆械,警方通過查閱死者的電腦和手機(jī)薇溃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缭乘,“玉大人沐序,你說我怎么就攤上這事《榧ǎ” “怎么了策幼?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長奴紧。 經(jīng)常有香客問我特姐,道長,這世上最難降的妖魔是什么黍氮? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任到逊,我火速辦了婚禮,結(jié)果婚禮上滤钱,老公的妹妹穿的比我還像新娘。我一直安慰自己脑题,他們只是感情好件缸,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著叔遂,像睡著了一般他炊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上已艰,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天痊末,我揣著相機(jī)與錄音,去河邊找鬼哩掺。 笑死凿叠,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的嚼吞。 我是一名探鬼主播盒件,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼舱禽!你這毒婦竟也來了炒刁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤誊稚,失蹤者是張志新(化名)和其女友劉穎翔始,沒想到半個(gè)月后罗心,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡城瞎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年渤闷,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片全谤。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡肤晓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出认然,到底是詐尸還是另有隱情补憾,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布卷员,位于F島的核電站盈匾,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏毕骡。R本人自食惡果不足惜削饵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望未巫。 院中可真熱鬧窿撬,春花似錦、人聲如沸叙凡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽握爷。三九已至跛璧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間新啼,已是汗流浹背追城。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留燥撞,地道東北人座柱。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像物舒,于是被迫代替她去往敵國和親辆布。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348