Ver código fonte

feat: 增加RippleView测试

DoggyZhang 1 mês atrás
pai
commit
b5d0cc992f

+ 1 - 0
app/src/main/AndroidManifest.xml

@@ -50,6 +50,7 @@
             android:stateNotNeeded="true"
             android:theme="@style/zxing_CaptureTheme"
             android:windowSoftInputMode="stateAlwaysHidden"/>
+        <activity android:name=".widget.RippleActivity"/>
 
     </application>
 

+ 5 - 0
app/src/main/java/com/adealink/frame/MainActivity.kt

@@ -21,6 +21,7 @@ import com.adealink.frame.svga.recyclerview.SvgaRecyclerViewActivity
 import com.adealink.frame.svga.viewpager.SvgaViewpagerActivity
 import com.adealink.frame.vap.VapActivity
 import com.adealink.frame.vap.VapEffectActivity
+import com.adealink.frame.widget.RippleActivity
 import com.adealink.frame.zxing.ZXingActivity
 import com.wenext.frame.effectpreview.EffectPreviewSettingActivity
 
@@ -114,6 +115,10 @@ class MainActivity : AppCompatActivity() {
             startActivity(Intent(this, ZXingActivity::class.java))
         }
 
+        findViewById<View>(R.id.tv_widget_ripple_test).setOnClickListener {
+            startActivity(Intent(this, RippleActivity::class.java))
+        }
+
         floatKitManager.install(application, true)
         floatKitManager.onMainIconDoubleClick = {
             Toast.makeText(application, "double click", Toast.LENGTH_SHORT).show()

+ 29 - 0
app/src/main/java/com/adealink/frame/widget/RippleActivity.kt

@@ -0,0 +1,29 @@
+package com.adealink.frame.widget
+
+import android.os.Bundle
+import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.isVisible
+import com.adealink.frame.R
+import com.adealink.frame.widget.ripple.ripple.RippleView
+
+class RippleActivity : AppCompatActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_widget_ripple)
+
+        findViewById<RippleView>(R.id.v_ripple).apply {
+
+        }
+        findViewById<View>(R.id.v_ripple_tag).apply {
+            setOnClickListener {
+                if (isVisible) {
+                    visibility = View.INVISIBLE
+                } else {
+                    visibility = View.VISIBLE
+                }
+            }
+        }
+    }
+}

+ 6 - 0
app/src/main/java/com/adealink/frame/widget/ripple/ripple/RippleCircle.kt

@@ -0,0 +1,6 @@
+package com.adealink.frame.widget.ripple.ripple
+
+/**
+ * 圆参数信息
+ */
+data class RippleCircle(var radius: Float, var alpha: Int)

+ 378 - 0
app/src/main/java/com/adealink/frame/widget/ripple/ripple/RippleView.kt

@@ -0,0 +1,378 @@
+package com.adealink.frame.widget.ripple.ripple
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.os.Build
+import android.util.AttributeSet
+import android.util.Log
+import android.view.View
+import android.view.WindowManager
+import androidx.annotation.ColorInt
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.LifecycleOwner
+import com.adealink.frame.R
+import com.adealink.frame.util.DisplayUtil
+import com.adealink.weparty.commonui.ripple.lifecyle.RippleLifecycle
+import com.adealink.frame.widget.ripple.ripple.lifecyle.RippleLifecycleAdapter
+import java.util.LinkedList
+import kotlin.math.min
+
+/**
+ * 水波纹扩散View
+ * https://github.com/Leo199206/RippleView
+ */
+class RippleView : View {
+    constructor(context: Context?) : this(context, null)
+    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
+    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
+        context,
+        attrs,
+        defStyleAttr
+    ) {
+        initAttributes(attrs)
+        initPaint()
+        if (isStart) {
+            onStart()
+        }
+    }
+
+    companion object {
+        private const val MAX_ALPHA = 255
+        private const val ALPHA_RANGE = 200
+    }
+
+    private var paint = Paint()
+
+    //private var circleMaxRadius: Int = 0
+    private var circleList: MutableList<RippleCircle> = LinkedList()
+    private var isStart: Boolean = false
+    private var isPause: Boolean = false
+
+    //private var circleCenterX: Float = 0f
+    //private var circleCenterY: Float = 0f
+    private val rippleLifecycle: RippleLifecycle by lazy {
+        RippleLifecycle(this)
+    }
+
+
+    @ColorInt
+    private var circleColor: Int = Color.RED
+    private var circleMinRadius: Float = 0f
+    private var circleCount: Int = 5
+    private var circleStyle: Paint.Style = Paint.Style.FILL
+    private var speed: Float = 0.5f
+
+    private var duration: Int = 200
+    private var circleStrokeWidth: Float = 3f
+
+
+    /**
+     * 配置参数初始化
+     * @param attrs AttributeSet?
+     */
+    private fun initAttributes(attrs: AttributeSet?) {
+        val array = context.obtainStyledAttributes(attrs, R.styleable.RippleView)
+        for (index in 0 until array.indexCount) {
+            when (val indexedValue = array.getIndex(index)) {
+                R.styleable.RippleView_ripple_circle_color -> {
+                    circleColor = array.getColor(indexedValue, Color.RED)
+                }
+
+                R.styleable.RippleView_ripple_circle_min_radius -> {
+                    circleMinRadius = array.getDimension(indexedValue, 0f)
+                }
+
+                R.styleable.RippleView_ripple_circle_count -> {
+                    circleCount = array.getInt(indexedValue, 2)
+                }
+
+                R.styleable.RippleView_ripple_circle_duration -> {
+                    duration = array.getInt(indexedValue, duration)
+                }
+
+                R.styleable.RippleView_ripple_circle_stroke_width -> {
+                    circleStrokeWidth = array.getDimension(indexedValue, circleStrokeWidth)
+                }
+
+                R.styleable.RippleView_ripple_circle_style -> {
+                    circleStyle = array.getInt(indexedValue, Paint.Style.FILL.ordinal).let {
+                        if (it == Paint.Style.FILL.ordinal) {
+                            Paint.Style.FILL
+                        } else {
+                            Paint.Style.STROKE
+                        }
+                    }
+                }
+
+                R.styleable.RippleView_ripple_circle_start -> {
+                    isStart = array.getBoolean(indexedValue, false)
+                }
+            }
+        }
+    }
+
+    fun setRipple(
+        color: Int,
+        minRadius: Float,
+        circleCount: Int,
+        speed: Float,
+        strokeWidth: Float
+    ) {
+        circleColor = color
+        circleMinRadius = minRadius
+        this.circleCount = circleCount
+        this.speed = speed
+        circleStrokeWidth = strokeWidth
+
+        initPaint()
+//        circleList.clear()
+//        initCircle()
+//        if (isStart) {
+//            onStart()
+//        }
+    }
+
+
+    /**
+     * 圆形半径、透明度参数初始化
+     */
+    private fun initCircle() {
+        //circleMaxRadius = (width / 2 - circleStrokeWidth).toInt()
+//        circleCenterX = width / 2f
+//        circleCenterY = height / 2f
+        circleList.clear()
+        circleList.add(RippleCircle(circleMinRadius, MAX_ALPHA))
+    }
+
+    private fun initSpeed() {
+        val refreshRate = getDisplayRefreshRate(context)
+        val refreshInterval = 1000f / refreshRate
+        val refreshCount = duration / refreshInterval
+        speed = (circleMaxRadius() - circleMinRadius) / refreshCount
+        Log.d(
+            "zhangfei",
+            "refreshRate:$refreshRate, refreshInterval:$refreshInterval, refreshCount:$refreshCount, " +
+                    "\nspeed:$speed, circleMaxRadius:${circleMaxRadius()}"
+        )
+    }
+
+    private var refreshRate: Int = 0
+    fun getDisplayRefreshRate(context: Context): Int {
+        if (refreshRate != 0) {
+            return refreshRate
+        }
+        val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            context.display.refreshRate
+        } else {
+            ContextCompat.getSystemService(
+                context,
+                WindowManager::class.java
+            )?.defaultDisplay?.refreshRate ?: 60f
+        }
+        refreshRate = display.toInt()
+        return refreshRate
+    }
+
+    private fun circleMaxRadius(): Int {
+        return (width / 2 - circleStrokeWidth).toInt()
+    }
+
+    /**
+     * 画笔初始化
+     */
+    private fun initPaint() {
+        paint.style = circleStyle
+        paint.strokeWidth = circleStrokeWidth
+        paint.isDither = true
+        paint.isAntiAlias = true
+        paint.color = circleColor
+    }
+
+
+    /**
+     * 测量控件尺寸
+     * @param widthMeasureSpec Int
+     * @param heightMeasureSpec Int
+     */
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        val defaultWidth = 200
+        var defaultHeight = 200
+        val width = measureSize(widthMeasureSpec, defaultWidth)
+        val height = measureSize(heightMeasureSpec, defaultHeight)
+        val size = min(width, height)
+        setMeasuredDimension(size, size)
+    }
+
+    /**
+     * 测量尺寸
+     *
+     * @param measureSpec
+     * @param defaultSize
+     * @return
+     */
+    private fun measureSize(measureSpec: Int, defaultSize: Int): Int {
+        var result: Int
+        val mode = MeasureSpec.getMode(measureSpec)
+        val size = MeasureSpec.getSize(measureSpec)
+        if (mode == MeasureSpec.EXACTLY) {
+            result = size
+        } else {
+            result = defaultSize
+            if (mode == MeasureSpec.AT_MOST) {
+                result = result.coerceAtMost(size)
+            }
+        }
+        return result
+    }
+
+
+    /**
+     * 尺寸变动回调
+     * @param w Int
+     * @param h Int
+     * @param oldw Int
+     * @param oldh Int
+     */
+    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+        super.onSizeChanged(w, h, oldw, oldh)
+        initCircle()
+        initSpeed()
+    }
+
+    /**
+     * 视图绘制
+     * @param canvas Canvas
+     */
+    override fun onDraw(canvas: Canvas) {
+        super.onDraw(canvas)
+        onDrawRippleCircle(canvas)
+    }
+
+
+    /**
+     * 绘制水波纹
+     * @param canvas Canvas
+     */
+    private fun onDrawRippleCircle(canvas: Canvas) {
+        if (!isStart) {
+            return
+        }
+
+        Log.d(
+            "zhangfei",
+            "onDrawRippleCircle, width:$width, height:$height, circleMaxRadius:${circleMaxRadius()}"
+        )
+        var i = 0
+
+        with(circleList.iterator()) {
+            while (hasNext()) {
+                i++
+                next().also {
+                    Log.d("zhangfei", "      $i -> , alpha:${it.alpha}, radius:${it.radius}, ")
+
+                    paint.alpha = it.alpha
+                    canvas.drawCircle(
+                        width / 2f,
+                        //circleCenterX,
+                        height / 2f,
+                        //circleCenterY,
+                        it.radius,
+                        paint
+                    )
+                    it.radius += speed
+                    if (it.radius > circleMaxRadius()) {
+                        remove()
+                    } else {
+                        var length = (circleMaxRadius().toFloat() - circleMinRadius)
+                        if (length <= 0) {
+                            length = 1f
+                        }
+                        var currentLength = it.radius - circleMinRadius
+                        if (currentLength <= 0) {
+                            currentLength = 0f
+                        }
+                        val percent = currentLength / length
+                        it.alpha =
+                            (MAX_ALPHA - (percent) * ALPHA_RANGE).toInt()
+                    }
+                }
+            }
+            addNewRippleCircle()
+        }
+        postInvalidate()
+    }
+
+
+    /**
+     * 添加新水波纹
+     */
+    private fun addNewRippleCircle() {
+        if (circleList.size <= 0) {
+            return
+        }
+        val minMeet = (circleMaxRadius() - circleMinRadius) / circleCount
+        val add = circleList.last().radius > (minMeet + circleMinRadius)
+        Log.d(
+            "zhangfei",
+            "      addNewRippleCircle(add:$add), minMeet:$minMeet, circleMinRadius:$circleMinRadius"
+        )
+        if (add) {
+            circleList.add(RippleCircle(circleMinRadius, MAX_ALPHA))
+        }
+    }
+
+
+    /**
+     * 绑定页面生命周期,自动进行资源释放
+     * @param lifecycleOwner LifecycleOwner?
+     */
+    private fun bindLifecycle(lifecycleOwner: LifecycleOwner?) {
+        lifecycleOwner
+            ?.lifecycle
+            ?.addObserver(RippleLifecycleAdapter(rippleLifecycle))
+    }
+
+    /**
+     * 开始播放动画
+     * @param lifecycleOwner LifecycleOwner?
+     */
+    fun onStart(lifecycleOwner: LifecycleOwner? = null) {
+        bindLifecycle(lifecycleOwner)
+        isStart = true
+        circleList.add(RippleCircle(circleMinRadius, MAX_ALPHA))
+        postInvalidate()
+    }
+
+    /**
+     * 动画暂停后,恢复动画播放
+     */
+    fun onResume() {
+        if (isPause) {
+            isStart = true
+            isPause = false
+            postInvalidate()
+        }
+    }
+
+    /**
+     * 停止播放水波纹动画
+     */
+    fun onStop() {
+        isPause = false
+        isStart = false
+        circleList.clear()
+    }
+
+    /**
+     * 暂停水波纹动画播放
+     */
+    fun onPause() {
+        if (isStart) {
+            isPause = true
+            isStart = false
+        }
+    }
+
+}

+ 23 - 0
app/src/main/java/com/adealink/frame/widget/ripple/ripple/lifecyle/BaseLifecycle.kt

@@ -0,0 +1,23 @@
+package com.adealink.frame.widget.ripple.ripple.lifecyle
+
+/**
+ *  Base Lifecycle Observer
+ */
+interface BaseLifecycle {
+
+    /**
+     * onResume
+     */
+    fun onResume()
+
+    /**
+     * onPause
+     */
+    fun onPause()
+
+    /**
+     * onDestroy
+     */
+    fun onDestroy() {
+    }
+}

+ 22 - 0
app/src/main/java/com/adealink/frame/widget/ripple/ripple/lifecyle/RippleLifecycle.kt

@@ -0,0 +1,22 @@
+package com.adealink.weparty.commonui.ripple.lifecyle
+
+import com.adealink.frame.widget.ripple.ripple.RippleView
+import com.adealink.frame.widget.ripple.ripple.lifecyle.BaseLifecycle
+import java.lang.ref.WeakReference
+
+/**
+ */
+class RippleLifecycle(view: RippleView) : BaseLifecycle {
+    private val reference = WeakReference(view)
+    override fun onResume() {
+        reference.get()?.onResume()
+    }
+
+    override fun onPause() {
+        reference.get()?.onPause()
+    }
+
+    override fun onDestroy() {
+        reference.get()?.onStop()
+    }
+}

+ 42 - 0
app/src/main/java/com/adealink/frame/widget/ripple/ripple/lifecyle/RippleLifecycleAdapter.kt

@@ -0,0 +1,42 @@
+package com.adealink.frame.widget.ripple.ripple.lifecyle
+
+import android.util.Log
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.OnLifecycleEvent
+
+
+/**
+ */
+class RippleLifecycleAdapter(private val lifecycle: BaseLifecycle) : LifecycleObserver {
+
+
+    /**
+     * onResume
+     */
+    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
+    fun onLifecycleResume() {
+        Log.i("${javaClass.simpleName}", "onLifecycleResume")
+        lifecycle.onResume()
+    }
+
+
+    /**
+     * onPause
+     */
+    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+    fun onLifecyclePause() {
+        Log.i("${javaClass.simpleName}", "onLifecyclePause")
+        lifecycle.onPause()
+    }
+
+
+    /**
+     * onDestroy
+     */
+    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+    fun onLifecycleDestroy() {
+        Log.i("${javaClass.simpleName}", "onLifecycleDestroy")
+        lifecycle.onDestroy()
+    }
+}

+ 11 - 0
app/src/main/res/layout/activity_main.xml

@@ -186,5 +186,16 @@
             android:textSize="16sp"
             tools:ignore="HardcodedText" />
 
+        <androidx.appcompat.widget.AppCompatButton
+            android:id="@+id/tv_widget_ripple_test"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center"
+            android:paddingHorizontal="12dp"
+            android:paddingVertical="8dp"
+            android:text="波纹控件"
+            android:textSize="16sp"
+            tools:ignore="HardcodedText" />
+
     </androidx.appcompat.widget.LinearLayoutCompat>
 </ScrollView>

+ 29 - 0
app/src/main/res/layout/activity_widget_ripple.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.adealink.frame.widget.ripple.ripple.RippleView
+        android:id="@+id/v_ripple"
+        style="@style/CommonOnlineRipple"
+        android:layout_width="400px"
+        android:layout_height="400px"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:ripple_circle_min_radius="160px"
+        app:ripple_circle_start="true" />
+
+    <View
+        android:id="@+id/v_ripple_tag"
+        android:layout_width="320px"
+        android:layout_height="320px"
+        android:background="@drawable/common_red_dot_normal_bg"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 15 - 0
app/src/main/res/values/attrs.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <declare-styleable name="RippleView">
+        <attr name="ripple_circle_color" format="color" />
+        <attr name="ripple_circle_min_radius" format="dimension" />
+        <attr name="ripple_circle_count" format="integer" />
+        <attr name="ripple_circle_style" format="enum">
+            <enum name="FILL" value="0" />
+            <enum name="STROKE" value="1" />
+        </attr>
+        <attr name="ripple_circle_stroke_width" format="dimension" />
+        <attr name="ripple_circle_start" format="boolean" />
+        <attr name="ripple_circle_duration" format="integer" />
+    </declare-styleable>
+</resources>

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -1,3 +1,5 @@
 <resources>
     <string name="app_name">WeNextFrame</string>
+
+
 </resources>

+ 8 - 0
app/src/main/res/values/styles.xml

@@ -56,4 +56,12 @@
         <item name="dot_text_text_color">@color/white</item>
     </style>
 
+    <style name="CommonOnlineRipple">
+        <item name="ripple_circle_color">#FF15E5E2</item>
+        <item name="ripple_circle_style">STROKE</item>
+        <item name="ripple_circle_duration">800</item>
+        <item name="ripple_circle_count">2</item>
+        <item name="ripple_circle_stroke_width">2px</item>
+    </style>
+
 </resources>