無意在某設計網站看到一個這樣的設計。留下了很深的印象。
然後,我自己嘗試的實現了一下,另外豐富了一下效果,如下:
這個控制元件支援設定顏色,支援是否允許小於0。使用Kotlin實現。
好像目前在購物車常用到這個效果。
這個控制元件主要用到了Google新出物理彈性動畫SpringAnimation。SpringAnimation 類是最近(25.3.0版本)才新增在支援庫中的一個類,它可以很方便的實現彈簧效果,支援的屬性有
TRANSLATION_X
TRANSLATION_Y
TRANSLATION_Z
SCALE_X
SCALE_Y
ROTATION
ROTATION_X
ROTATION_Y
X
Y
Z
ALPHA
SCROLL_X
SCROLL_Y
複製程式碼
如果是一個現有的控制元件,想要實現彈簧效果,可以參考(http://www.jianshu.com/p/c2962a8135f5)這個地址。建議先讀過上面這篇文章,再讀本文。
本文是一個非常規的使用方式,主要是使用了SpringAnimation計算結果,更新中間小球及整個控制元件的位置。
首先上完整程式碼:
package com.greendami.ppcountview
import android.content.Context
import android.graphics.*
import android.support.animation.DynamicAnimation
import android.support.animation.SpringAnimation
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.View
import kotlinx.android.synthetic.main.activity_main.view.*
/**
* 計數器
* Created by GreendaMi on 2017/7/31.
*/
class PPCountView(context: Context?, attrs: AttributeSet?) : DynamicAnimation.OnAnimationUpdateListener, View(context, attrs) {
var lenth = height / 8f
var color: Int = 0
set(value) {
field = value
postInvalidate()
}
var canDownzaro = true
set(value) {
field = value
postInvalidate()
}
var centerX: Float = 0.toFloat()
set(value) {
field = if (value + measuredWidth / 2f <= measuredHeight / 2) measuredHeight / 2f
else if (value + measuredWidth / 2f >= measuredWidth - measuredHeight / 2f) measuredWidth - measuredHeight / 2f else value + measuredWidth / 2f
postInvalidate()
}
var count: Int = 0//顯示的數字
private var textPaint: Paint = Paint()
private var mPaint: Paint = Paint()
private var velocityTracker: VelocityTracker = VelocityTracker.obtain()
private var downX: Float = 0.toFloat()
val animX = SpringAnimation(this, SpringAnimation.TRANSLATION_X, 0f)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
setBackgroundColor(Color.TRANSPARENT)
initPaint()
lenth = measuredHeight / 8f
centerX = 0f
if (color == 0) {
color = context.resources.getColor(R.color.colorPrimary)
}
animX.addUpdateListener(this)
animX.spring.stiffness = getStiffness()
animX.spring.dampingRatio = getDamping()
}
private fun initPaint() {
textPaint.reset()
mPaint.reset()
mPaint.color = color
mPaint.isAntiAlias = true
textPaint.color = Color.WHITE
textPaint.textSize = height / 2f
textPaint.strokeWidth = height / 18f
textPaint.isAntiAlias = true
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
drawBackground(canvas)
drawBackCenterCircle(canvas)
}
private fun drawBackCenterCircle(canvas: Canvas?) {
//畫陰影
var mRadialGradient = RadialGradient(centerX, height / 2f, (height / 2f) * 1.16f, intArrayOf(Color.GRAY, Color.TRANSPARENT), null,
Shader.TileMode.REPEAT)
mPaint.shader = mRadialGradient
//計算陰影半徑
var r = if (centerX > width / 2) Math.min(width - centerX, height / 2f * 1.15f) else Math.min(centerX, height / 2f * 1.15f)
canvas?.drawCircle(centerX, height / 2f, r, mPaint)
//畫圓
mPaint.reset()
mPaint.color = Color.WHITE
mPaint.isAntiAlias = true
canvas?.drawCircle(centerX, height / 2f, height / 2f * 0.95f, mPaint)
//寫數字
textPaint.color = color
canvas?.drawText(count.toString(), centerX - textPaint.measureText(count.toString()) / 2f, (height - textPaint.ascent() - textPaint.descent()) / 2f, textPaint)
}
private fun drawBackground(canvas: Canvas?) {
initPaint()
canvas?.drawRoundRect(0f, 0f, width.toFloat(), height.toFloat(), height / 2f, height / 2f, mPaint)
textPaint.color = Color.WHITE
canvas?.drawLine(height / 2f - lenth, height / 2f, height / 2f + lenth, height / 2f, textPaint)
canvas?.drawLine((width - height / 2f) - lenth, height / 2f, (width - height / 2f) + lenth, height / 2f, textPaint)
canvas?.drawLine(width - height / 2f, height / 2f - lenth, width - height / 2f, height / 2f + lenth, textPaint)
}
override fun onAnimationUpdate(animation: DynamicAnimation<out DynamicAnimation<*>>?, value: Float, velocity: Float) {
box.centerX = value
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
downX = event.rawX
velocityTracker.addMovement(event)
return true
}
MotionEvent.ACTION_MOVE -> {
animX.cancel()
this.centerX = (event.rawX - downX)
this.translationX = (event.rawX - downX)
velocityTracker.addMovement(event)
return true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
velocityTracker.computeCurrentVelocity(500)
if (box.translationX !== 0f) {
animX.setStartVelocity(velocityTracker.getXVelocity())
animX.start()
}
if (event.rawX > downX) {
count++
} else {
if (count <= 0 && canDownzaro) count--
if (count <= 0 && !canDownzaro) count = 0
if (count > 0) count--
}
velocityTracker.clear()
return true
}
}
return false
}
private fun getDamping(): Float {
return 0.4f
}
private fun getStiffness(): Float {
return 50f
}
}
複製程式碼
gradle是這樣的,我用的是AS3.0。所以引入方式有點不同,但這不是本文的重點。
apply plugin: `com.android.application`
apply plugin: `kotlin-android`
apply plugin: `kotlin-android-extensions`
android {
compileSdkVersion 26
buildToolsVersion "26.0.1"
defaultConfig {
applicationId "com.greendami.ppcountview"
minSdkVersion 21
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile(`proguard-android.txt`), `proguard-rules.pro`
}
}
}
dependencies {
implementation fileTree(dir: `libs`, include: [`*.jar`])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation `com.android.support:appcompat-v7:26.0.0-beta1`
implementation `com.android.support.constraint:constraint-layout:1.0.2`
testImplementation `junit:junit:4.12`
androidTestImplementation `com.android.support.test:runner:0.5`
androidTestImplementation `com.android.support.test.espresso:espresso-core:2.2.2`
implementation `com.android.support:support-dynamic-animation:26.0.0-beta1`
}
複製程式碼
Main.java中設定顏色,和是否支援負數。
package com.greendami.ppcountview
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// box.color = Color.GREEN
box.canDownzaro = true
}
}
複製程式碼
佈局檔案是這樣的。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
tools:context="com.greendami.ppcountview.MainActivity">
<com.greendami.ppcountview.PPCountView
android:id="@+id/box"
android:layout_width="130dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:background="@color/colorPrimary" />
</LinearLayout>
複製程式碼
程式碼中比較重點的就是DynamicAnimation.OnAnimationUpdateListener,實現了監聽器,得到SpringAnimation的計算結果。然後將結果當做座標,然後重繪小球。
//宣告一個動畫,第一個引數是要執行動畫的View,第二個動畫是需要被計算的屬性,這裡是X軸的移動,第三個引數是動畫的結束位置。
val animX = SpringAnimation(this, SpringAnimation.TRANSLATION_X, 0f)
複製程式碼
//設定動畫監聽器
animX.addUpdateListener(this)
//設定彈性的生硬度,stiffness值越小,彈簧越容易擺動,擺動的時間越長,反之擺動時間越短
animX.spring.stiffness = getStiffness()
//方法設定彈性阻尼,dampingRatio越大,擺動次數越少,當到1的時候完全不擺動
animX.spring.dampingRatio = getDamping()
複製程式碼
在ontouch中
MotionEvent.ACTION_MOVE -> {
animX.cancel()
this.centerX = (event.rawX - downX)
this.translationX = (event.rawX - downX)
velocityTracker.addMovement(event)
return true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
velocityTracker.computeCurrentVelocity(500)
if (box.translationX !== 0f) {
animX.setStartVelocity(velocityTracker.getXVelocity())
animX.start()
}
if (event.rawX > downX) {
count++
} else {
if (count <= 0 && canDownzaro) count--
if (count <= 0 && !canDownzaro) count = 0
if (count > 0) count--
}
velocityTracker.clear()
return true
}
複製程式碼
滑動的時候,控制元件跟隨手指移動,鬆手後,給動畫賦一個加速度,然後開始動畫,此時之前設定的監聽器收到了事件。監聽器中收到計算後的數值,然後賦值給centerX。當centerX改變後
var centerX: Float = 0.toFloat()
set(value) {
field = if (value + measuredWidth / 2f <= measuredHeight / 2) measuredHeight / 2f
else if (value + measuredWidth / 2f >= measuredWidth - measuredHeight / 2f) measuredWidth - measuredHeight / 2f else value + measuredWidth / 2f
postInvalidate()
}
複製程式碼
要保證小球座標的位置不會超出整個控制元件的大小,最後呼叫重繪。
最後說一下小球的陰影繪製,陰影的範圍要實時計算,當小球運動到兩端的時候,是沒有陰影的。
//畫陰影
var mRadialGradient = RadialGradient(centerX, height / 2f, (height / 2f) * 1.16f, intArrayOf(Color.GRAY, Color.TRANSPARENT), null,Shader.TileMode.REPEAT)
mPaint.shader = mRadialGradient
//計算陰影半徑
var r = if (centerX > width / 2) Math.min(width - centerX, height / 2f * 1.15f) else Math.min(centerX, height / 2f * 1.15f)
canvas?.drawCircle(centerX, height / 2f, r, mPaint)
複製程式碼