├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── github │ │ └── leavesc │ │ └── customview │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── github │ │ │ └── leavesc │ │ │ └── customview │ │ │ ├── CircleRefreshViewActivity.kt │ │ │ ├── MainActivity.kt │ │ │ ├── ViewActivity.kt │ │ │ ├── WaveViewActivity.kt │ │ │ ├── base │ │ │ ├── BaseActivity.kt │ │ │ └── BaseView.kt │ │ │ ├── utils │ │ │ └── Utils.kt │ │ │ ├── view │ │ │ ├── CircleRefreshView.kt │ │ │ ├── PointBeatView.kt │ │ │ ├── TaiJiView.kt │ │ │ ├── WaveLoadingView.kt │ │ │ └── WaveView.kt │ │ │ └── widget │ │ │ └── OnSeekBarChangeSimpleListener.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_circle_refresh_view.xml │ │ ├── activity_main.xml │ │ ├── activity_point_beat_view.xml │ │ ├── activity_tai_ji_view.xml │ │ ├── activity_wave_loading_view.xml │ │ └── activity_wave_view.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── github │ └── leavesc │ └── customview │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-android-extensions' 5 | } 6 | 7 | android { 8 | compileSdkVersion 30 9 | buildToolsVersion "30.0.3" 10 | defaultConfig { 11 | applicationId "github.leavesc.customview" 12 | minSdkVersion 21 13 | targetSdkVersion 30 14 | versionCode 1 15 | versionName "1.0" 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | kotlinOptions { 29 | jvmTarget = '1.8' 30 | } 31 | } 32 | 33 | dependencies { 34 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 35 | testImplementation 'junit:junit:4.13.2' 36 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 37 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 38 | implementation 'androidx.core:core-ktx:1.3.2' 39 | implementation 'androidx.appcompat:appcompat:1.2.0' 40 | implementation 'com.google.android.material:material:1.2.1' 41 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 42 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/github/leavesc/customview/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package github.leavesc.customview 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("github.leavesc.customview", appContext.packageName) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/github/leavesc/customview/CircleRefreshViewActivity.kt: -------------------------------------------------------------------------------- 1 | package github.leavesc.customview 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.widget.SeekBar 6 | import github.leavesc.customview.base.BaseActivity 7 | import github.leavesc.customview.widget.OnSeekBarChangeSimpleListener 8 | import kotlinx.android.synthetic.main.activity_circle_refresh_view.* 9 | 10 | class CircleRefreshViewActivity : BaseActivity() { 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | setContentView(R.layout.activity_circle_refresh_view) 15 | seekBarDrag.max = 100 16 | seekBarDrag.setOnSeekBarChangeListener(object : OnSeekBarChangeSimpleListener() { 17 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 18 | circleRefreshView.drag(progress / 100f) 19 | } 20 | }) 21 | seekBarSeed.max = 100 22 | seekBarSeed.setOnSeekBarChangeListener(object : OnSeekBarChangeSimpleListener() { 23 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 24 | circleRefreshView.speed = (progress * 40).toLong() 25 | } 26 | }) 27 | seekBarSeed.progress = (circleRefreshView.speed / 40).toInt() 28 | } 29 | 30 | fun onClick(view: View) { 31 | when (view.id) { 32 | R.id.btnStart -> { 33 | circleRefreshView.startAnimator() 34 | } 35 | R.id.btnStop -> { 36 | circleRefreshView.stopAnimator() 37 | } 38 | } 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesc/customview/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package github.leavesc.customview 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import github.leavesc.customview.base.BaseActivity 6 | 7 | class MainActivity : BaseActivity() { 8 | 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | setContentView(R.layout.activity_main) 12 | } 13 | 14 | fun startWaveViewActivity(view: View) { 15 | startActivity(WaveViewActivity::class.java) 16 | } 17 | 18 | fun startWaveLoadingViewActivity(view: View) { 19 | ViewActivity.navTo(this, R.layout.activity_wave_loading_view) 20 | } 21 | 22 | fun startCircleRefreshViewActivity(view: View) { 23 | startActivity(CircleRefreshViewActivity::class.java) 24 | } 25 | 26 | fun startPointBeatViewActivity(view: View) { 27 | ViewActivity.navTo(this, R.layout.activity_point_beat_view) 28 | } 29 | 30 | fun startTaiJiViewActivity(view: View) { 31 | ViewActivity.navTo(this, R.layout.activity_tai_ji_view) 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesc/customview/ViewActivity.kt: -------------------------------------------------------------------------------- 1 | package github.leavesc.customview 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.annotation.LayoutRes 6 | import github.leavesc.customview.base.BaseActivity 7 | 8 | class ViewActivity : BaseActivity() { 9 | 10 | companion object { 11 | 12 | private const val KEY_LAYOUT_ID = "keyLayoutId" 13 | 14 | fun navTo(activity: BaseActivity, @LayoutRes layoutId: Int) { 15 | val intent = Intent(activity, ViewActivity::class.java) 16 | intent.putExtra(KEY_LAYOUT_ID, layoutId) 17 | activity.startActivity(intent) 18 | } 19 | 20 | } 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | setContentView(intent.getIntExtra(KEY_LAYOUT_ID, 0)) 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesc/customview/WaveViewActivity.kt: -------------------------------------------------------------------------------- 1 | package github.leavesc.customview 2 | 3 | import android.os.Bundle 4 | import android.widget.SeekBar 5 | import github.leavesc.customview.base.BaseActivity 6 | import github.leavesc.customview.utils.Utils 7 | import github.leavesc.customview.widget.OnSeekBarChangeSimpleListener 8 | import kotlinx.android.synthetic.main.activity_wave_view.* 9 | 10 | class WaveViewActivity : BaseActivity() { 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | setContentView(R.layout.activity_wave_view) 15 | 16 | seekBarWidth.max = 100 17 | seekBarWidth.setOnSeekBarChangeListener(object : OnSeekBarChangeSimpleListener() { 18 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 19 | val scale = progress / 100.0f 20 | waveView.waveWidthScale = scale 21 | } 22 | }) 23 | seekBarWidth.progress = 100 24 | 25 | seekBarHeight.max = 100 26 | seekBarHeight.setOnSeekBarChangeListener(object : OnSeekBarChangeSimpleListener() { 27 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 28 | val scale = progress / 100.0f * 0.1f 29 | waveView.waveHeightScale = scale 30 | } 31 | }) 32 | seekBarHeight.progress = 35 33 | 34 | seekBarSpeed.max = 4000 35 | seekBarSpeed.setOnSeekBarChangeListener(object : OnSeekBarChangeSimpleListener() { 36 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 37 | val speed = (progress + 300).toLong() 38 | waveView.speed = speed 39 | } 40 | }) 41 | seekBarSpeed.progress = 500 42 | 43 | seekBarColor.max = 100 44 | seekBarColor.setOnSeekBarChangeListener(object : OnSeekBarChangeSimpleListener() { 45 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 46 | waveView.bgColor = Utils.getRandomColorInt() 47 | } 48 | }) 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesc/customview/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package github.leavesc.customview.base 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.util.Log 6 | import android.widget.Toast 7 | import androidx.appcompat.app.AppCompatActivity 8 | 9 | open class BaseActivity : AppCompatActivity() { 10 | 11 | private val tag = this::class.java.name 12 | 13 | protected fun startActivity(clazz: Class) { 14 | startActivity(Intent(this, clazz)) 15 | } 16 | 17 | protected fun showToast(msg: Any) { 18 | Toast.makeText(this, msg.toString(), Toast.LENGTH_SHORT).show() 19 | } 20 | 21 | protected fun log(log: Any?) { 22 | Log.e(tag, log.toString()) 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesc/customview/base/BaseView.kt: -------------------------------------------------------------------------------- 1 | package github.leavesc.customview.base 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.util.Log 6 | import android.view.View 7 | 8 | open class BaseView @JvmOverloads constructor( 9 | context: Context, attrs: AttributeSet? = null, 10 | defStyleAttr: Int = 0, defStyleRes: Int = 0 11 | ) : View(context, attrs, defStyleAttr, defStyleRes) { 12 | 13 | @JvmField 14 | protected val tag = this.javaClass.simpleName 15 | 16 | protected val screenWidth: Int 17 | get() = resources.displayMetrics.widthPixels 18 | 19 | protected val screenHeight: Int 20 | get() = resources.displayMetrics.heightPixels 21 | 22 | protected fun getSize(measureSpec: Int, defaultSize: Int): Int { 23 | return when (MeasureSpec.getMode(measureSpec)) { 24 | MeasureSpec.AT_MOST -> { 25 | MeasureSpec.getSize(measureSpec).coerceAtMost(defaultSize) 26 | } 27 | MeasureSpec.EXACTLY -> { 28 | MeasureSpec.getSize(measureSpec) 29 | } 30 | MeasureSpec.UNSPECIFIED -> { 31 | defaultSize 32 | } 33 | else -> { 34 | defaultSize 35 | } 36 | } 37 | } 38 | 39 | protected fun dp2px(dpValue: Number): Int { 40 | return (dpValue.toDouble() * context.resources.displayMetrics.density + 0.5f).toInt() 41 | } 42 | 43 | protected fun px2dp(pxValue: Number): Int { 44 | return (pxValue.toDouble() / context.resources.displayMetrics.density + 0.5f).toInt() 45 | } 46 | 47 | protected fun sp2px(spValue: Number): Int { 48 | return (spValue.toDouble() * context.resources.displayMetrics.scaledDensity + 0.5f).toInt() 49 | } 50 | 51 | protected fun px2sp(pxValue: Number): Int { 52 | return (pxValue.toDouble() / context.resources.displayMetrics.scaledDensity + 0.5f).toInt() 53 | } 54 | 55 | fun log(log: Any?) { 56 | Log.e(tag, log?.toString() ?: "null") 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesc/customview/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package github.leavesc.customview.utils 2 | 3 | import android.graphics.Color 4 | import java.util.* 5 | import kotlin.random.Random 6 | 7 | object Utils { 8 | 9 | fun getRandomColor(): String { 10 | var r: String = Integer.toHexString(Random.nextInt(256)).toUpperCase(Locale.ROOT) 11 | var g: String = Integer.toHexString(Random.nextInt(256)).toUpperCase(Locale.ROOT) 12 | var b: String = Integer.toHexString(Random.nextInt(256)).toUpperCase(Locale.ROOT) 13 | r = if (r.length == 1) "0$r" else r 14 | g = if (g.length == 1) "0$g" else g 15 | b = if (b.length == 1) "0$b" else b 16 | return "#$r$g$b" 17 | } 18 | 19 | fun getRandomColorInt(): Int { 20 | var r: String = Integer.toHexString(Random.nextInt(256)).toUpperCase(Locale.ROOT) 21 | var g: String = Integer.toHexString(Random.nextInt(256)).toUpperCase(Locale.ROOT) 22 | var b: String = Integer.toHexString(Random.nextInt(256)).toUpperCase(Locale.ROOT) 23 | r = if (r.length == 1) "0$r" else r 24 | g = if (g.length == 1) "0$g" else g 25 | b = if (b.length == 1) "0$b" else b 26 | return Color.parseColor("#$r$g$b") 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesc/customview/view/CircleRefreshView.kt: -------------------------------------------------------------------------------- 1 | package github.leavesc.customview.view 2 | 3 | import android.animation.Animator 4 | import android.animation.ValueAnimator 5 | import android.content.Context 6 | import android.graphics.Canvas 7 | import android.graphics.Color 8 | import android.graphics.Paint 9 | import android.util.AttributeSet 10 | import android.view.animation.LinearInterpolator 11 | import github.leavesc.customview.base.BaseView 12 | 13 | class CircleRefreshView @JvmOverloads constructor( 14 | context: Context, 15 | attrs: AttributeSet? = null, 16 | defStyleAttr: Int = 0, 17 | defStyleRes: Int = 0 18 | ) : BaseView(context, attrs, defStyleAttr, defStyleRes) { 19 | 20 | private data class Circle(var x: Float, var y: Float, var radius: Float, var color: Int) 21 | 22 | companion object { 23 | 24 | private const val LEFT = 0 25 | 26 | private const val RIGHT = 1 27 | 28 | private const val CENTER = 2 29 | 30 | //View的默认宽度,dp 31 | private const val DEFAULT_WIDTH = 80 32 | 33 | //View的默认高度,dp 34 | private const val DEFAULT_HEIGHT = 50 35 | 36 | //每个圆的默认最大半径 37 | private const val DEFAULT_MAX_RADIUS = 8 38 | 39 | //每个圆的默认最小半径 40 | private const val DEFAULT_MIN_RADIUS = 6 41 | 42 | //默认速度 43 | private const val DEFAULT_SPEED = 3000L 44 | 45 | } 46 | 47 | var speed = DEFAULT_SPEED 48 | set(value) { 49 | field = value 50 | animator.duration = value 51 | } 52 | 53 | private var contentWidth = 0 54 | 55 | private var contentHeight = 0 56 | 57 | //中心圆与相邻两个圆的圆心间隔 58 | private var gap = 0 59 | 60 | //圆最大半径 61 | private var maxRadius = dp2px(DEFAULT_MAX_RADIUS) 62 | 63 | //圆最小半径 64 | private var minRadius = dp2px(DEFAULT_MIN_RADIUS) 65 | 66 | //绿色 67 | private val colorCircleLeft = Color.parseColor("#008577") 68 | 69 | //橙色 70 | private val colorCircleCenter = Color.parseColor("#f96630") 71 | 72 | //红色 73 | private val colorCircleRight = Color.parseColor("#f54183") 74 | 75 | private val circleList = mutableListOf() 76 | 77 | private val paint = Paint().apply { 78 | isAntiAlias = true 79 | style = Paint.Style.FILL 80 | } 81 | 82 | private val animator = ValueAnimator.ofFloat(0f, 1f).apply { 83 | duration = speed 84 | repeatCount = -1 85 | repeatMode = ValueAnimator.RESTART 86 | interpolator = LinearInterpolator() 87 | addListener(object : Animator.AnimatorListener { 88 | override fun onAnimationStart(animation: Animator) { 89 | //当动画启动时,将三个圆的位置重置到准备开启动画的临界状态 90 | resetToStart() 91 | } 92 | 93 | override fun onAnimationEnd(animation: Animator) {} 94 | override fun onAnimationCancel(animation: Animator) {} 95 | override fun onAnimationRepeat(animation: Animator) {} 96 | }) 97 | addUpdateListener { animation: ValueAnimator -> 98 | //循环刷新三个圆的位置 99 | for (i in circleList.indices) { 100 | updateCircle(i, animation.animatedFraction) 101 | } 102 | invalidate() 103 | } 104 | } 105 | 106 | //将三个圆的位置重置到准备开启动画的临界状态 107 | private fun resetToStart() { 108 | var circle = circleList[LEFT] 109 | circle.x = minRadius.toFloat() 110 | circle.radius = minRadius.toFloat() 111 | circle = circleList[RIGHT] 112 | circle.x = (contentWidth - minRadius).toFloat() 113 | circle.radius = minRadius.toFloat() 114 | circle = circleList[CENTER] 115 | circle.x = (contentWidth shr 1).toFloat() 116 | circle.radius = maxRadius.toFloat() 117 | invalidate() 118 | } 119 | 120 | private fun updateCircle(index: Int, fraction: Float) { 121 | // x x x 122 | // ------------||-------------||--------------||------------ 123 | // 1/4 2/4 3/4 124 | // 1/4 2/4 3/4 4/4 125 | 126 | // 左边-绿色 127 | // 半径从0到min 半径从min到max 半径从max到min 半径从min到0 128 | 129 | // 中间-橙色 130 | // 半径从max到min 半径从min到0 131 | //半径从0到min 半径从min到max 132 | 133 | // 右边-红色 134 | // 半径从min到0 135 | //半径从0到min 半径从min到max 半径从max到min 136 | var radius = 0f 137 | var x = 0f 138 | when (index) { 139 | LEFT -> { 140 | when { 141 | fraction <= 1f / 4f -> { 142 | radius = minRadius * (4f * fraction) 143 | x = minRadius.toFloat() 144 | } 145 | fraction <= 0.5f -> { 146 | val percent = (fraction - 1f / 4f) * 4f 147 | radius = minRadius + percent * (maxRadius - minRadius) 148 | x = minRadius + percent * (contentWidth / 2f - minRadius) 149 | } 150 | fraction <= 3f / 4f -> { 151 | val percent = (fraction - 0.5f) * 4f 152 | radius = maxRadius - percent * (maxRadius - minRadius) 153 | x = contentWidth / 2f + percent * (contentWidth / 2f - minRadius) 154 | } 155 | else -> { 156 | radius = minRadius - (fraction - 3f / 4f) * 4f * minRadius 157 | x = (contentWidth - minRadius).toFloat() 158 | } 159 | } 160 | } 161 | CENTER -> { 162 | when { 163 | fraction <= 1f / 4f -> { 164 | val percent = fraction * 4f 165 | radius = maxRadius - (maxRadius - minRadius) * percent 166 | x = contentWidth / 2f + (contentWidth / 2f - minRadius) * percent 167 | } 168 | fraction <= 0.5f -> { 169 | radius = minRadius - (fraction - 1f / 4f) * 4f * minRadius 170 | x = (contentWidth - minRadius).toFloat() 171 | } 172 | fraction <= 3f / 4f -> { 173 | radius = minRadius * (4f * (fraction - 0.5f)) 174 | x = minRadius.toFloat() 175 | } 176 | else -> { 177 | val percent = (fraction - 3f / 4f) * 4f 178 | radius = minRadius + (maxRadius - minRadius) * percent 179 | x = minRadius + (contentWidth / 2f - minRadius) * percent 180 | } 181 | } 182 | } 183 | RIGHT -> { 184 | when { 185 | fraction <= 1f / 4f -> { 186 | radius = minRadius - 4f * fraction * minRadius 187 | x = (contentWidth - minRadius).toFloat() 188 | } 189 | fraction <= 0.5f -> { 190 | radius = minRadius * (4f * (fraction - 1f / 4f)) 191 | x = minRadius.toFloat() 192 | } 193 | fraction <= 3f / 4f -> { 194 | val percent = (fraction - 0.5f) * 4f 195 | radius = minRadius + (maxRadius - minRadius) * percent 196 | x = minRadius + (contentWidth / 2f - minRadius) * percent 197 | } 198 | else -> { 199 | val percent = (fraction - 3f / 4f) * 4f 200 | radius = maxRadius - (maxRadius - minRadius) * percent 201 | x = contentWidth / 2f + (contentWidth / 2f - minRadius) * percent 202 | } 203 | } 204 | } 205 | } 206 | val circle = circleList[index] 207 | circle.radius = radius 208 | circle.x = x 209 | } 210 | 211 | override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { 212 | super.onLayout(changed, left, top, right, bottom) 213 | contentWidth = measuredWidth - paddingLeft - paddingRight 214 | contentHeight = measuredHeight - paddingTop - paddingBottom 215 | resetCircles() 216 | } 217 | 218 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 219 | val width = getSize(widthMeasureSpec, dp2px(DEFAULT_WIDTH)) 220 | val height = getSize(heightMeasureSpec, dp2px(DEFAULT_HEIGHT)) 221 | setMeasuredDimension(width, height) 222 | } 223 | 224 | override fun onDraw(canvas: Canvas) { 225 | for (circle in circleList) { 226 | paint.color = circle.color 227 | canvas.drawCircle( 228 | circle.x + paddingLeft, circle.y + paddingTop, circle.radius, 229 | paint 230 | ) 231 | } 232 | } 233 | 234 | fun drag(fraction: Float) { 235 | if (animator.isRunning) { 236 | return 237 | } 238 | if (fraction > 1) { 239 | return 240 | } 241 | circleList[LEFT].x = minRadius + gap * (1f - fraction) 242 | circleList[RIGHT].x = (contentWidth / 2f) + gap * fraction 243 | invalidate() 244 | } 245 | 246 | private fun resetCircles() { 247 | val x = contentWidth / 2f 248 | val y = contentHeight / 2f 249 | if (circleList.isEmpty()) { 250 | gap = (x - minRadius).toInt() 251 | val circleLeft = Circle( 252 | x, y, 253 | minRadius.toFloat(), colorCircleLeft 254 | ) 255 | val circleCenter = Circle( 256 | x, y, 257 | maxRadius.toFloat(), colorCircleCenter 258 | ) 259 | val circleRight = Circle( 260 | x, y, 261 | minRadius.toFloat(), colorCircleRight 262 | ) 263 | circleList.add(LEFT, circleLeft) 264 | circleList.add(RIGHT, circleRight) 265 | circleList.add(CENTER, circleCenter) 266 | } else { 267 | for (i in circleList.indices) { 268 | val circle = circleList[i] 269 | circle.x = x 270 | circle.y = y 271 | if (i == CENTER) { 272 | circle.radius = maxRadius.toFloat() 273 | } else { 274 | circle.radius = minRadius.toFloat() 275 | } 276 | } 277 | } 278 | } 279 | 280 | override fun onDetachedFromWindow() { 281 | super.onDetachedFromWindow() 282 | stopAnimator() 283 | animator.removeAllUpdateListeners() 284 | } 285 | 286 | fun startAnimator() { 287 | if (!animator.isRunning) { 288 | animator.start() 289 | } 290 | } 291 | 292 | fun stopAnimator() { 293 | if (animator.isRunning) { 294 | animator.cancel() 295 | resetCircles() 296 | invalidate() 297 | } 298 | } 299 | 300 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesc/customview/view/PointBeatView.kt: -------------------------------------------------------------------------------- 1 | package github.leavesc.customview.view 2 | 3 | import android.animation.Animator 4 | import android.animation.AnimatorListenerAdapter 5 | import android.animation.ValueAnimator 6 | import android.content.Context 7 | import android.graphics.Canvas 8 | import android.graphics.Color 9 | import android.graphics.Paint 10 | import android.graphics.Path 11 | import android.util.AttributeSet 12 | import android.view.View 13 | import android.view.animation.AccelerateInterpolator 14 | import android.view.animation.DecelerateInterpolator 15 | import github.leavesc.customview.base.BaseView 16 | 17 | class PointBeatView @JvmOverloads constructor( 18 | context: Context, 19 | attrs: AttributeSet? = null, 20 | defStyleAttr: Int = 0, 21 | defStyleRes: Int = 0 22 | ) : BaseView(context, attrs, defStyleAttr, defStyleRes) { 23 | 24 | private class Point { 25 | var x = 0f 26 | var y = 0f 27 | var radius = 0f 28 | } 29 | 30 | //小球 31 | private val ballPoint = Point() 32 | 33 | //贝塞尔曲线控制点 34 | private val controlPoint = Point() 35 | 36 | private var lineY = 0f 37 | 38 | private var lineXLeft = 0f 39 | 40 | private var lineXRight = 0f 41 | 42 | //小球最高点Y坐标 43 | private var pointYMin = 0f 44 | 45 | private val paint = Paint().apply { 46 | isAntiAlias = true 47 | isDither = true 48 | } 49 | 50 | private val path = Path() 51 | 52 | private val downAnimator = ValueAnimator().apply { 53 | //加速下降 54 | interpolator = AccelerateInterpolator() 55 | addUpdateListener { animation: ValueAnimator -> 56 | ballPoint.y = animation.animatedValue as Float 57 | if (ballPoint.y + ballPoint.radius <= lineY) { 58 | controlPoint.y = lineY 59 | } else { 60 | controlPoint.y = lineY + 2 * (ballPoint.y + ballPoint.radius - lineY) 61 | } 62 | invalidate() 63 | } 64 | addListener(object : AnimatorListenerAdapter() { 65 | override fun onAnimationEnd(animation: Animator) { 66 | startUpAnimator() 67 | } 68 | }) 69 | } 70 | 71 | private val upAnimator = ValueAnimator().apply { 72 | //减速上升 73 | interpolator = DecelerateInterpolator() 74 | addUpdateListener { animation: ValueAnimator -> 75 | ballPoint.y = animation.animatedValue as Float 76 | if (ballPoint.y + ballPoint.radius >= lineY) { //还处于水平线以下 77 | controlPoint.y = lineY + 2 * (ballPoint.y + ballPoint.radius - lineY) 78 | } else { 79 | //小球总的要上升的距离 80 | val tempY = lineY - pointYMin 81 | //小球最低点距离水平线的距离,即小球已上升的距离 82 | val distance = lineY - ballPoint.y - ballPoint.radius 83 | //上升比例 84 | val percentage = distance / tempY 85 | when { 86 | percentage <= 0.2 -> { //线从水平线升高到最高点 87 | controlPoint.y = lineY + 2 * (ballPoint.y + ballPoint.radius - lineY) 88 | } 89 | percentage <= 0.28 -> { //线从最高点降落到水平线 90 | controlPoint.y = lineY - (distance - tempY * 0.2f) 91 | } 92 | percentage <= 0.34 -> { //线从水平线降落到最低点 93 | controlPoint.y = lineY + (distance - tempY * 0.28f) 94 | } 95 | percentage <= 0.39 -> { //线从最低点升高到水平线 96 | controlPoint.y = lineY - (distance - tempY * 0.34f) 97 | } 98 | else -> { 99 | controlPoint.y = lineY 100 | } 101 | } 102 | } 103 | invalidate() 104 | } 105 | addListener(object : AnimatorListenerAdapter() { 106 | override fun onAnimationEnd(animation: Animator) { 107 | startDownAnimator() 108 | } 109 | }) 110 | } 111 | 112 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 113 | val width = getSize(widthMeasureSpec, screenWidth) 114 | val height = getSize(heightMeasureSpec, screenHeight) 115 | setMeasuredDimension(width, height) 116 | } 117 | 118 | override fun onSizeChanged(contentWidth: Int, contentHeight: Int, oldW: Int, oldH: Int) { 119 | super.onSizeChanged(contentWidth, contentHeight, oldW, oldH) 120 | lineY = contentHeight * 0.5f 121 | lineXLeft = contentWidth * 0.15f 122 | lineXRight = contentWidth * 0.85f 123 | 124 | //小球最低点Y坐标 125 | val pointYMax = contentHeight * 0.55f 126 | pointYMin = contentHeight * 0.22f 127 | ballPoint.x = contentWidth * 0.5f 128 | ballPoint.radius = 26f 129 | ballPoint.y = pointYMin 130 | controlPoint.x = ballPoint.x 131 | val speed: Long = 1500 132 | downAnimator.setFloatValues(pointYMin, pointYMax) 133 | upAnimator.setFloatValues(pointYMax, pointYMin) 134 | downAnimator.duration = speed 135 | upAnimator.duration = (0.8 * speed).toLong() 136 | startAnimator() 137 | } 138 | 139 | override fun onDraw(canvas: Canvas) { 140 | paint.color = Color.WHITE 141 | paint.strokeWidth = 8f 142 | path.reset() 143 | path.moveTo(lineXLeft, lineY) 144 | path.quadTo(controlPoint.x, controlPoint.y, lineXRight, lineY) 145 | paint.style = Paint.Style.STROKE 146 | canvas.drawPath(path, paint) 147 | paint.style = Paint.Style.FILL 148 | canvas.drawCircle(lineXLeft, lineY, 16f, paint) 149 | canvas.drawCircle(lineXRight, lineY, 16f, paint) 150 | paint.color = Color.parseColor("#f7584d") 151 | paint.strokeWidth = 0f 152 | canvas.drawCircle(ballPoint.x, ballPoint.y, ballPoint.radius, paint) 153 | } 154 | 155 | override fun onDetachedFromWindow() { 156 | super.onDetachedFromWindow() 157 | stopAnimator() 158 | } 159 | 160 | override fun onVisibilityChanged(changedView: View, visibility: Int) { 161 | super.onVisibilityChanged(changedView, visibility) 162 | when (visibility) { 163 | VISIBLE -> { 164 | startAnimator() 165 | } 166 | INVISIBLE, GONE -> { 167 | stopAnimator() 168 | } 169 | } 170 | } 171 | 172 | private fun startAnimator() { 173 | startDownAnimator() 174 | } 175 | 176 | private fun stopAnimator() { 177 | stopDownAnimator() 178 | stopUpAnimator() 179 | } 180 | 181 | private fun startDownAnimator() { 182 | if (downAnimator.values != null && downAnimator.values.isNotEmpty() && !downAnimator.isRunning) { 183 | downAnimator.start() 184 | } 185 | } 186 | 187 | private fun stopDownAnimator() { 188 | if (downAnimator.isRunning) { 189 | downAnimator.cancel() 190 | } 191 | } 192 | 193 | private fun startUpAnimator() { 194 | if (upAnimator.values != null && upAnimator.values.isNotEmpty() && !upAnimator.isRunning) { 195 | upAnimator.start() 196 | } 197 | } 198 | 199 | private fun stopUpAnimator() { 200 | if (upAnimator.isRunning) { 201 | upAnimator.cancel() 202 | } 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /app/src/main/java/github/leavesc/customview/view/TaiJiView.kt: -------------------------------------------------------------------------------- 1 | package github.leavesc.customview.view 2 | 3 | import android.animation.ValueAnimator 4 | import android.content.Context 5 | import android.graphics.Canvas 6 | import android.graphics.Color 7 | import android.graphics.Paint 8 | import android.graphics.RectF 9 | import android.util.AttributeSet 10 | import android.view.animation.LinearInterpolator 11 | import github.leavesc.customview.base.BaseView 12 | 13 | class TaiJiView @JvmOverloads constructor( 14 | context: Context, attrs: AttributeSet? = null, 15 | defStyleAttr: Int = 0, defStyleRes: Int = 0 16 | ) : BaseView(context, attrs, defStyleAttr, defStyleRes) { 17 | 18 | private var radius = 0f 19 | 20 | private var centerX = 0f 21 | 22 | private var centerY = 0f 23 | 24 | private val paint = Paint().apply { 25 | isAntiAlias = true 26 | isDither = true 27 | } 28 | 29 | private val rectF = RectF() 30 | 31 | private var degreesAnimatedValue = 0f 32 | 33 | private val valueAnimator = ValueAnimator().apply { 34 | setFloatValues(0f, 360f) 35 | duration = 3000 36 | repeatCount = ValueAnimator.INFINITE 37 | interpolator = LinearInterpolator() 38 | addUpdateListener { animation: ValueAnimator -> 39 | this@TaiJiView.degreesAnimatedValue = animation.animatedValue as Float 40 | invalidate() 41 | } 42 | } 43 | 44 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 45 | val width = getSize(widthMeasureSpec, screenWidth).coerceAtMost( 46 | getSize( 47 | heightMeasureSpec, 48 | screenHeight 49 | ) 50 | ) 51 | setMeasuredDimension(width, width) 52 | } 53 | 54 | override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) { 55 | val width = (w - paddingLeft - paddingRight).coerceAtMost(h - paddingTop - paddingBottom) 56 | radius = width / 2f 57 | centerX = paddingLeft + radius 58 | centerY = paddingTop + radius 59 | } 60 | 61 | override fun onDraw(canvas: Canvas) { 62 | val realRadius = radius 63 | val halfRadius = realRadius / 2f 64 | val pointRadius = halfRadius / 8f 65 | 66 | canvas.translate(centerX, centerY) 67 | canvas.rotate(degreesAnimatedValue) 68 | 69 | //绘制边框 70 | paint.color = Color.BLACK 71 | paint.style = Paint.Style.STROKE 72 | paint.strokeWidth = 1f 73 | canvas.drawCircle(0f, 0f, realRadius, paint) 74 | 75 | //绘制左右半圆 76 | rectF.set(-realRadius, -realRadius, realRadius, realRadius) 77 | paint.style = Paint.Style.FILL 78 | paint.strokeWidth = 0f 79 | paint.color = Color.BLACK 80 | canvas.drawArc(rectF, 90f, 180f, true, paint) 81 | paint.color = Color.WHITE 82 | canvas.drawArc(rectF, -90f, 180f, true, paint) 83 | 84 | //绘制上边的白色圆 85 | canvas.save() 86 | canvas.translate(0f, -halfRadius) 87 | paint.color = Color.WHITE 88 | paint.style = Paint.Style.FILL 89 | paint.strokeWidth = 1f 90 | canvas.drawCircle(0f, 0f, halfRadius, paint) 91 | paint.color = Color.BLACK 92 | canvas.drawCircle(0f, 0f, pointRadius, paint) 93 | canvas.restore() 94 | 95 | //绘制上边的黑色圆 96 | canvas.save() 97 | canvas.translate(0f, halfRadius) 98 | paint.color = Color.BLACK 99 | paint.style = Paint.Style.FILL 100 | canvas.drawCircle(0f, 0f, halfRadius, paint) 101 | paint.color = Color.WHITE 102 | canvas.drawCircle(0f, 0f, pointRadius, paint) 103 | canvas.restore() 104 | } 105 | 106 | override fun onAttachedToWindow() { 107 | super.onAttachedToWindow() 108 | startAnimator() 109 | } 110 | 111 | override fun onDetachedFromWindow() { 112 | super.onDetachedFromWindow() 113 | stopAnimator() 114 | valueAnimator.removeAllUpdateListeners() 115 | } 116 | 117 | private fun startAnimator() { 118 | if (!valueAnimator.isRunning) { 119 | valueAnimator.start() 120 | } 121 | } 122 | 123 | private fun stopAnimator() { 124 | if (valueAnimator.isRunning) { 125 | valueAnimator.cancel() 126 | } 127 | } 128 | 129 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesc/customview/view/WaveLoadingView.kt: -------------------------------------------------------------------------------- 1 | package github.leavesc.customview.view 2 | 3 | import android.animation.ValueAnimator 4 | import android.content.Context 5 | import android.graphics.* 6 | import android.util.AttributeSet 7 | import android.view.View 8 | import android.view.animation.LinearInterpolator 9 | import androidx.annotation.ColorInt 10 | import github.leavesc.customview.R 11 | import github.leavesc.customview.base.BaseView 12 | 13 | class WaveLoadingView @JvmOverloads constructor( 14 | context: Context, attrs: AttributeSet? = null, 15 | defStyleAttr: Int = 0, defStyleRes: Int = 0 16 | ) : BaseView(context, attrs, defStyleAttr, defStyleRes) { 17 | 18 | companion object { 19 | 20 | //每个波浪的宽度占据View宽度的默认比例 21 | private const val DEFAULT_WAVE_SCALE_WIDTH = 0.8f 22 | 23 | //每个波浪的高度占据View高度的默认比例 24 | private const val DEFAULT_WAVE_SCALE_HEIGHT = 0.13f 25 | 26 | //波浪的默认颜色 27 | private val DEFAULT_WAVE_COLOR = Color.parseColor("#f54183") 28 | 29 | //文本下方的默认颜色 30 | private const val DEFAULT_DOWN_TEXT_COLOR = Color.WHITE 31 | 32 | //默认文本大小,sp 33 | private const val DEFAULT_TEXT_SIZE = 150 34 | 35 | //波浪的默认速度 36 | private const val DEFAULT_SPEED = 900L 37 | 38 | //View的默认大小,dp 39 | private const val DEFAULT_SIZE = 220 40 | 41 | } 42 | 43 | private var waveScaleWidth = 0f 44 | 45 | private var waveScaleHeight = 0f 46 | 47 | @ColorInt 48 | private var waveColor = 0 49 | 50 | @ColorInt 51 | private var downTextColor = 0 52 | 53 | //每个波浪的起伏高度 54 | private var waveHeight = 0f 55 | 56 | //每个波浪的宽度 57 | private var waveWidth = 0f 58 | 59 | //波浪的速度 60 | private var speed = 0L 61 | 62 | private var textSize = 0f 63 | 64 | private var text = 0.toChar() 65 | 66 | private var size = 0f 67 | 68 | private var animatedValue = 0f 69 | 70 | private val circlePath = Path() 71 | 72 | private val wavePath = Path() 73 | 74 | private var radius = 0f 75 | 76 | private var centerX = 0f 77 | 78 | private var centerY = 0f 79 | 80 | private val valueAnimator = ValueAnimator().apply { 81 | duration = speed 82 | repeatCount = ValueAnimator.INFINITE 83 | interpolator = LinearInterpolator() 84 | addUpdateListener { animation: ValueAnimator -> 85 | this@WaveLoadingView.animatedValue = animation.animatedValue as Float 86 | invalidate() 87 | } 88 | } 89 | 90 | init { 91 | initAttributeSet(context, attrs) 92 | resetWaveParams() 93 | } 94 | 95 | private val wavePaint = Paint().apply { 96 | isAntiAlias = true 97 | isDither = true 98 | color = waveColor 99 | style = Paint.Style.FILL 100 | strokeWidth = 0f 101 | } 102 | 103 | private val textPaint = Paint().apply { 104 | isAntiAlias = true 105 | isDither = true 106 | style = Paint.Style.FILL 107 | typeface = Typeface.DEFAULT_BOLD 108 | textSize = this@WaveLoadingView.textSize 109 | } 110 | 111 | private fun initAttributeSet(context: Context, attrs: AttributeSet?) { 112 | val array = context.obtainStyledAttributes(attrs, R.styleable.WaveLoadingView) 113 | waveScaleWidth = 114 | array.getFloat(R.styleable.WaveLoadingView_scaleWidth, DEFAULT_WAVE_SCALE_WIDTH) 115 | waveScaleHeight = 116 | array.getFloat(R.styleable.WaveLoadingView_scaleHeight, DEFAULT_WAVE_SCALE_HEIGHT) 117 | waveColor = array.getColor(R.styleable.WaveLoadingView_waveColor, DEFAULT_WAVE_COLOR) 118 | downTextColor = 119 | array.getColor(R.styleable.WaveLoadingView_downTextColor, DEFAULT_DOWN_TEXT_COLOR) 120 | textSize = array.getDimension( 121 | R.styleable.WaveLoadingView_textSize, 122 | sp2px(DEFAULT_TEXT_SIZE).toFloat() 123 | ) 124 | speed = array.getInt(R.styleable.WaveLoadingView_speed, DEFAULT_SPEED.toInt()).toLong() 125 | val centerText = array.getString(R.styleable.WaveLoadingView_centerText) 126 | if (!centerText.isNullOrBlank()) { 127 | text = centerText[0] 128 | } 129 | array.recycle() 130 | } 131 | 132 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 133 | val defaultSize = dp2px(DEFAULT_SIZE) 134 | var width = getSize(widthMeasureSpec, defaultSize) 135 | var height = getSize(heightMeasureSpec, defaultSize) 136 | height = width.coerceAtMost(height) 137 | width = height 138 | setMeasuredDimension(width, height) 139 | } 140 | 141 | override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) { 142 | super.onSizeChanged(w, h, oldW, oldH) 143 | size = w.coerceAtMost(h).toFloat() 144 | radius = size / 2f 145 | centerX = radius 146 | centerY = radius 147 | resetWaveParams() 148 | } 149 | 150 | override fun onDraw(canvas: Canvas) { 151 | textPaint.color = waveColor 152 | drawText(canvas, textPaint, text.toString()) 153 | wavePath.reset() 154 | wavePath.moveTo(-waveWidth + animatedValue, size / 2.2f) 155 | var i = -waveWidth 156 | while (i < size + waveWidth) { 157 | wavePath.rQuadTo(waveWidth / 4f, -waveHeight, waveWidth / 2f, 0f) 158 | wavePath.rQuadTo(waveWidth / 4f, waveHeight, waveWidth / 2f, 0f) 159 | i += waveWidth 160 | } 161 | wavePath.lineTo(size, size) 162 | wavePath.lineTo(0f, size) 163 | wavePath.close() 164 | circlePath.reset() 165 | circlePath.addCircle(centerX, centerY, radius - 1, Path.Direction.CCW) 166 | circlePath.op(wavePath, Path.Op.INTERSECT) 167 | canvas.drawPath(circlePath, wavePaint) 168 | canvas.clipPath(circlePath) 169 | textPaint.color = downTextColor 170 | drawText(canvas, textPaint, text.toString()) 171 | } 172 | 173 | private fun drawText(canvas: Canvas, textPaint: Paint, text: String) { 174 | val rect = RectF(0f, 0f, size, size) 175 | textPaint.textAlign = Paint.Align.CENTER 176 | val fontMetrics = textPaint.fontMetrics 177 | val top = fontMetrics.top 178 | val bottom = fontMetrics.bottom 179 | val centerY = rect.centerY() - top / 2f - bottom / 2f 180 | canvas.drawText(text, rect.centerX(), centerY, textPaint) 181 | } 182 | 183 | private fun resetWaveParams() { 184 | waveWidth = size * waveScaleWidth 185 | waveHeight = size * waveScaleHeight 186 | valueAnimator.setFloatValues(0f, waveWidth) 187 | valueAnimator.duration = speed 188 | } 189 | 190 | override fun onAttachedToWindow() { 191 | super.onAttachedToWindow() 192 | startAnimator() 193 | } 194 | 195 | override fun onDetachedFromWindow() { 196 | super.onDetachedFromWindow() 197 | stopAnimator() 198 | valueAnimator.removeAllUpdateListeners() 199 | } 200 | 201 | override fun onVisibilityChanged(changedView: View, visibility: Int) { 202 | super.onVisibilityChanged(changedView, visibility) 203 | when (visibility) { 204 | VISIBLE -> { 205 | startAnimator() 206 | } 207 | INVISIBLE, GONE -> { 208 | stopAnimator() 209 | } 210 | } 211 | } 212 | 213 | private fun startAnimator() { 214 | if (!valueAnimator.isRunning) { 215 | valueAnimator.start() 216 | } 217 | } 218 | 219 | private fun stopAnimator() { 220 | if (valueAnimator.isRunning) { 221 | valueAnimator.cancel() 222 | } 223 | } 224 | 225 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesc/customview/view/WaveView.kt: -------------------------------------------------------------------------------- 1 | package github.leavesc.customview.view 2 | 3 | import android.animation.ValueAnimator 4 | import android.content.Context 5 | import android.graphics.Canvas 6 | import android.graphics.Color 7 | import android.graphics.Paint 8 | import android.graphics.Path 9 | import android.util.AttributeSet 10 | import android.view.View 11 | import android.view.animation.LinearInterpolator 12 | import github.leavesc.customview.base.BaseView 13 | 14 | class WaveView @JvmOverloads constructor( 15 | context: Context, attrs: AttributeSet? = null, 16 | defStyleAttr: Int = 0, defStyleRes: Int = 0 17 | ) : BaseView(context, attrs, defStyleAttr, defStyleRes) { 18 | 19 | companion object { 20 | 21 | //每个波浪的宽度占据 View 宽度的默认比例 22 | private const val DEFAULT_WAVE_SCALE_WIDTH = 1f 23 | 24 | //每个波浪的高度占据 View 高度的默认比例 25 | private const val DEFAULT_WAVE_SCALE_HEIGHT = 0.035f 26 | 27 | //波浪的默认速度 28 | private const val DEFAULT_SPEED = 800L 29 | 30 | //波浪的默认背景色 31 | private val DEFAULT_BG_COLOR = Color.parseColor("#FF018786") 32 | 33 | } 34 | 35 | private var contentWidth = 0 36 | 37 | private var contentHeight = 0 38 | 39 | //每个波浪的起伏高度 40 | private var waveHeight = 0f 41 | 42 | //每个波浪的宽度 43 | private var waveWidth = 0f 44 | 45 | //波浪的速度 46 | var speed = DEFAULT_SPEED 47 | set(value) { 48 | field = value 49 | resetWaveParams() 50 | } 51 | 52 | var bgColor = DEFAULT_BG_COLOR 53 | set(value) { 54 | field = value 55 | wavePaint.color = value 56 | } 57 | 58 | var waveWidthScale = 0f 59 | set(value) { 60 | if (value <= 0 || value > 1) { 61 | return 62 | } 63 | field = value 64 | resetWaveParams() 65 | } 66 | 67 | var waveHeightScale = 0f 68 | set(value) { 69 | if (value <= 0 || value > 1) { 70 | return 71 | } 72 | field = value 73 | resetWaveParams() 74 | } 75 | 76 | private var animatedValue = 0f 77 | 78 | private val wavePath = Path() 79 | 80 | private val wavePaint = Paint().apply { 81 | isAntiAlias = true 82 | isDither = true 83 | color = bgColor 84 | style = Paint.Style.FILL 85 | } 86 | 87 | private val valueAnimator = ValueAnimator().apply { 88 | duration = speed 89 | repeatCount = ValueAnimator.INFINITE 90 | interpolator = LinearInterpolator() 91 | addUpdateListener { animation: ValueAnimator -> 92 | this@WaveView.animatedValue = animation.animatedValue as Float 93 | invalidate() 94 | } 95 | } 96 | 97 | init { 98 | waveWidthScale = DEFAULT_WAVE_SCALE_WIDTH 99 | waveHeightScale = DEFAULT_WAVE_SCALE_HEIGHT 100 | } 101 | 102 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 103 | setMeasuredDimension( 104 | getSize(widthMeasureSpec, screenWidth), 105 | getSize(heightMeasureSpec, screenHeight) 106 | ) 107 | } 108 | 109 | override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) { 110 | super.onSizeChanged(width, height, oldWidth, oldHeight) 111 | contentWidth = width 112 | contentHeight = height 113 | resetWaveParams() 114 | } 115 | 116 | override fun onDraw(canvas: Canvas) { 117 | wavePath.reset() 118 | wavePath.moveTo(-waveWidth + animatedValue, contentHeight / 2f) 119 | var i = -waveWidth 120 | while (i < contentWidth + waveWidth) { 121 | wavePath.rQuadTo(waveWidth / 4f, -waveHeight, waveWidth / 2f, 0f) 122 | wavePath.rQuadTo(waveWidth / 4f, waveHeight, waveWidth / 2f, 0f) 123 | i += waveWidth 124 | } 125 | wavePath.lineTo(contentWidth.toFloat(), contentHeight.toFloat()) 126 | wavePath.lineTo(0f, contentHeight.toFloat()) 127 | wavePath.close() 128 | canvas.drawPath(wavePath, wavePaint) 129 | } 130 | 131 | private fun resetWaveParams() { 132 | waveWidth = contentWidth * waveWidthScale 133 | waveHeight = contentHeight * waveHeightScale 134 | valueAnimator.setFloatValues(0f, waveWidth) 135 | valueAnimator.duration = speed 136 | } 137 | 138 | override fun onAttachedToWindow() { 139 | super.onAttachedToWindow() 140 | startAnimator() 141 | } 142 | 143 | override fun onDetachedFromWindow() { 144 | super.onDetachedFromWindow() 145 | stopAnimator() 146 | valueAnimator.removeAllUpdateListeners() 147 | } 148 | 149 | override fun onVisibilityChanged(changedView: View, visibility: Int) { 150 | super.onVisibilityChanged(changedView, visibility) 151 | when (visibility) { 152 | VISIBLE -> { 153 | startAnimator() 154 | } 155 | INVISIBLE, GONE -> { 156 | stopAnimator() 157 | } 158 | } 159 | } 160 | 161 | private fun startAnimator() { 162 | if (!valueAnimator.isRunning) { 163 | valueAnimator.start() 164 | } 165 | } 166 | 167 | private fun stopAnimator() { 168 | if (valueAnimator.isRunning) { 169 | valueAnimator.cancel() 170 | } 171 | } 172 | 173 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesc/customview/widget/OnSeekBarChangeSimpleListener.kt: -------------------------------------------------------------------------------- 1 | package github.leavesc.customview.widget 2 | 3 | import android.widget.SeekBar 4 | import android.widget.SeekBar.OnSeekBarChangeListener 5 | 6 | open class OnSeekBarChangeSimpleListener : OnSeekBarChangeListener { 7 | 8 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 9 | 10 | } 11 | 12 | override fun onStartTrackingTouch(seekBar: SeekBar) { 13 | 14 | } 15 | 16 | override fun onStopTrackingTouch(seekBar: SeekBar) { 17 | 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_circle_refresh_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 |