Android鬼點子 Q彈的計數器

我是綠色大米呀發表於2019-01-23
靜態圖

無意在某設計網站看到一個這樣的設計。留下了很深的印象。

preview.gif

然後,我自己嘗試的實現了一下,另外豐富了一下效果,如下:

pic2.gif

這個控制元件支援設定顏色,支援是否允許小於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)
複製程式碼

相關文章