欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【Kotlin】一个有趣的商品数量加减交互控件(商详页或者购物车商品数量的加减)

程序员文章站 2022-05-22 19:54:04
...

前言

自从5月18号那天Google宣布Kotlin成为Android第一开发语言的时候,Kotlin就火了,然后再6月Kotlin就直接挤入编程语言排行前50,好吧,既然Google都这么做了,那我们也是不是应该适当的熟悉熟悉呢?所以,我想,接下来的我的博客应该都会尝试用Kotlin来开发,然后再说说遇到的问题和自己的感受吧。

既然本次开发使用Kotlin的,那么如果不熟悉或者不会在Android studio上用Kotlin开发的,可以先去看看我之前(去年)的博文
【Android】使用Kotlin在Android Studio上开发App

先看看今天我们要实现的效果,自己感受下吧,可能这边效果不是很棒
【Kotlin】一个有趣的商品数量加减交互控件(商详页或者购物车商品数量的加减)

引出BlinChengDemos

然后既然要用Kotlin了,那么我们就新建一个全新的项目吧,然后我想把我之前的所有东西都整合起来变成一个大的Demo,直接共享到github上供大家参考。然后该全新的项目当然是用Kotlin来开发啦~啦啦啦。看看结构目录
【Kotlin】一个有趣的商品数量加减交互控件(商详页或者购物车商品数量的加减)
嗯,大概就是这样的。然后很多东西其实我还没做,所以,先在这边提一下,等慢慢晚上了,我再来介绍。今天的主角可不是这个哦~

GoodAmountView

咱们就来看看我们今天怎么来实现吧,东西相当简单哈~其实真的如果没啥问题,大家应该都可以写出来,我今天提的主要是这个创意哈。其实开发那么久,比如我,商城类App写了又写~然后,到今天,发现,其实,我们做一个功能也可以这么有趣。之前我们在商详那边商品数量一般来说为了避免键盘操作,都直接是通过上面的 加减按钮来操作,然后在购物车,每次更改商品数量还可能会引发购物车价格活动因素的刷新,所以可想而知,当我一个商品想从1加到10的时候,你要点10次加号,然后要接受购物车的10次刷新。。。其实我是不能接受的,但是是在由于购物车的活动价格等复杂的因素,不得不有一个即时性。但是通过这种方式,拖动的方式,我们可以拖动到10,然后再松手,这个时候就之刷新一次了,是不~

实现方式

把那么多东西封装到一个空间里面,那就首先选择ViewGroup这样的喽,哈哈哈 直接上代码

/**
 * Author: Blincheng.
 * Date: 2017/6/9.
 * Description:一个有趣的商品数量加减交互控件
 */

class GoodAmountView : RelativeLayout, View.OnTouchListener, View.OnClickListener {
    var actionBack: OnActionBack?= null
    var ONCE_TIME = 160L //加减的时间间隔
    var ANIMATION_DURATION = 200L//恢复动画时间
    var startX = 0f
    var countView: LinearLayout?= null
    var onActionTime = 0L
    var isAction = false//是否正在被拖动(触发加减操作)
    var amount_tv: TextView?= null
    var add_img: ImageView?= null
    var sub_img: ImageView?= null
    var amount = 1//文本数量
    constructor(context: Context) : super(context) {
        init()
    }

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
        init()
    }

    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        init()
    }

    fun init(){
        LayoutInflater.from(context).inflate(R.layout.layout_good_amount_view, this, true)
        countView = findViewById(R.id.good_amount_linear) as LinearLayout
        countView?.setOnTouchListener(this)
        amount_tv = findViewById(R.id.good_amount_tv) as TextView
        add_img = findViewById(R.id.good_amount_add_btn) as ImageView
        sub_img = findViewById(R.id.good_amount_sub_btn) as ImageView
        add_img?.setOnClickListener(this)
        sub_img?.setOnClickListener(this)
    }
    override fun onClick(v: View?) {
        when(v?.id){
            R.id.good_amount_add_btn -> {
                amount ++
                amount_tv?.text = amount.toString()
                actionBack?.onAddAction()
            }
            R.id.good_amount_sub_btn -> {
                if(amount > 1)
                    amount --
                amount_tv?.text = amount.toString()
                actionBack?.onSubAction()
            }
        }
    }
    override fun onTouch(v: View?, event: MotionEvent?): Boolean {
        when(event?.action){
            MotionEvent.ACTION_DOWN -> {
                startX = event?.rawX
            }
            MotionEvent.ACTION_MOVE -> {
                var moveX = event?.rawX - startX
                moveAnimation(moveX.toInt())
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                backAnimation()
            }
        }
        return true
    }
    fun moveAnimation(x: Int){
        if (x in -UIUtils.dip2px(context,30f)..UIUtils.dip2px(context,30f)){
            var layout: LayoutParams = countView?.layoutParams as LayoutParams
            layout?.leftMargin = x+ UIUtils.dip2px(context,30f)
            isAction = false
            countView?.layoutParams = layout
            return
        }
        //一些回调
        if(isAction){
            if(x < -UIUtils.dip2px(context,25f)){//减
                actionCallBack(false)
            }else if(x > UIUtils.dip2px(context,25f)){//加
                actionCallBack(true)
            }
        }else{
            isAction = true
            onActionTime = Date().time
        }
    }
    fun backAnimation(){
        var layout: LayoutParams = countView?.layoutParams as LayoutParams
        var valueAnimator = ValueAnimator.ofFloat(layout.leftMargin.toFloat(), UIUtils.dip2px(context,30f).toFloat())
        valueAnimator.duration = ANIMATION_DURATION
        valueAnimator.interpolator = AccelerateInterpolator()
        valueAnimator.addUpdateListener { animation ->
            var layout: LayoutParams = countView?.layoutParams as LayoutParams
            layout.leftMargin = (animation.animatedValue as Float).toInt()
            countView?.layoutParams = layout
        }
        valueAnimator.addListener(object : Animator.AnimatorListener {
            override fun onAnimationRepeat(animation: Animator?) {
            }

            override fun onAnimationEnd(animation: Animator?) {
                actionBack?.onResult(amount)
            }

            override fun onAnimationCancel(animation: Animator?) {
            }

            override fun onAnimationStart(animation: Animator?) {
            }

        })
        valueAnimator.start()
    }
    fun actionCallBack(isAdd: Boolean){
        var now =  Date().time
        if(now - onActionTime > ONCE_TIME){
            if(isAdd){
                amount ++
                amount_tv?.text = amount.toString()
                actionBack?.onAddAction()
            }else{
                if(amount > 1)
                    amount --
                amount_tv?.text = amount.toString()
                actionBack?.onSubAction()
            }
            onActionTime = now
        }
    }
    fun setOnActionBackListener(onActionBack: OnActionBack){
        actionBack = onActionBack
    }
    interface OnActionBack{
        fun onAddAction()
        fun onSubAction()
        fun onResult(amount: Int)
    }
}

接下来就就是来分析一下,这边我选择使用了RelativeLayout来作为我们的最外层布局,通过加载一个layout布局文件。也可以简单看下Kotlin的语法哈,继承关系是通过:来处理的。然后必要的要重写实现的方法需要用constructor关键字。

fun init(){
        LayoutInflater.from(context).inflate(R.layout.layout_good_amount_view, this, true)
        countView = findViewById(R.id.good_amount_linear) as LinearLayout
        countView?.setOnTouchListener(this)
        amount_tv = findViewById(R.id.good_amount_tv) as TextView
        add_img = findViewById(R.id.good_amount_add_btn) as ImageView
        sub_img = findViewById(R.id.good_amount_sub_btn) as ImageView
        add_img?.setOnClickListener(this)
        sub_img?.setOnClickListener(this)
    }

我们通过直接LayoutInflater.from(context).inflate(R.layout.layout_good_amount_view, this, true)来加载这个布局,然后就是一些必要控件的初始化和绑定操作。

实现手指拖动效果

看效果图,其实我们只关心手指的X轴的移动距离,对吧。然后我的布局是RelativeLayout,所以最终是通过leftMargin这个属性的变化来实现移动的效果。

override fun onTouch(v: View?, event: MotionEvent?): Boolean {
        when(event?.action){
            MotionEvent.ACTION_DOWN -> {
                startX = event?.rawX
            }
            MotionEvent.ACTION_MOVE -> {
                var moveX = event?.rawX - startX
                moveAnimation(moveX.toInt())
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                backAnimation()
            }
        }
        return true
    }

我们再看看moveAnimation这个方法

fun moveAnimation(x: Int){
        if (x in -UIUtils.dip2px(context,30f)..UIUtils.dip2px(context,30f)){
            var layout: LayoutParams = countView?.layoutParams as LayoutParams
            layout?.leftMargin = x+ UIUtils.dip2px(context,30f)
            isAction = false
            countView?.layoutParams = layout
            return
        }
        //一些回调
        if(isAction){
            if(x < -UIUtils.dip2px(context,30f)){//减
                actionCallBack(false)
            }else if(x > UIUtils.dip2px(context,30f)){//加
                actionCallBack(true)
            }
        }else{
            isAction = true
            onActionTime = Date().time
        }
    }

看上面代码,注意if里面的return,也就是说,上面if里面是正常的拖动移动位置,当移动到一个边界值的时候,就触发我们的数量加减操作,并且不移动中间的文本。然后处理一些回调,回调的之后说明。

松手回弹动画

fun backAnimation(){
        var layout: LayoutParams = countView?.layoutParams as LayoutParams
        var valueAnimator = ValueAnimator.ofFloat(layout.leftMargin.toFloat(), UIUtils.dip2px(context,30f).toFloat())
        valueAnimator.duration = ANIMATION_DURATION
        valueAnimator.interpolator = AccelerateInterpolator()
        valueAnimator.addUpdateListener { animation ->
            var layout: LayoutParams = countView?.layoutParams as LayoutParams
            layout.leftMargin = (animation.animatedValue as Float).toInt()
            countView?.layoutParams = layout
        }
        valueAnimator.addListener(object : Animator.AnimatorListener {
            override fun onAnimationRepeat(animation: Animator?) {
            }

            override fun onAnimationEnd(animation: Animator?) {
                actionBack?.onResult(amount)
            }

            override fun onAnimationCancel(animation: Animator?) {
            }

            override fun onAnimationStart(animation: Animator?) {
            }

        })
        valueAnimator.start()
    }

这边就一个还原动画,那就借助ValueAnimator来实现呗。把一个当前的leftMargin和初始值传入,然后就让ValueAnimator来帮我们实现呗。layout.leftMargin = (animation.animatedValue as Float).toInt()这行代码我单独说下。。。因为这边animation是一个Object类型,刚开始我就直接as Int然后就报错了,需要知道的是Kotlin也是需要先把Object转成Float,然后再通过基本类型的toInt来转换。

数量变更

fun actionCallBack(isAdd: Boolean){
        var now =  Date().time
        if(now - onActionTime > ONCE_TIME){
            if(isAdd){
                amount ++
                amount_tv?.text = amount.toString()
                actionBack?.onAddAction()
            }else{
                if(amount > 1)
                    amount --
                amount_tv?.text = amount.toString()
                actionBack?.onSubAction()
            }
            onActionTime = now
        }
    }

因为数量的增加是看手指拖动的时间来决定的,所以我这边是采用这样的方式,先在边界的时候出发加减操作的第一次记录那个时候的时间,然后在每次move的时候通过时间的计算来看看是否大于ONCE_TIME,如果大于了,就该加加该减减。然后覆盖第一次触发的时间为当前时间。

应该需要的回调

interface OnActionBack{
        fun onAddAction()//加事件
        fun onSubAction()//减事件
        fun onResult(amount: Int)//最终回弹后回调事件
    }

本次Kotlin开发带来的一些感受

这边就来说说这次用Kotlin来开发和Java的区别哈~其实我自从去年那篇博文之后,也没有真正意义上用Kotlin来开发Android。
1. `声明真的变化很大

val ONCE_TIME = 160L //加减的时间间隔
    val ANIMATION_DURATION = 200L//恢复动画时间

这边这些常量的声明真的简单明了,全靠Kotlin自己推断,然后不习惯的就是变量的声明

var amount_tv: TextView?= null
var add_img: ImageView?= null
var sub_img: ImageView?= null

看看哈,以往怎么声明?后面一定要跟null吗?nonono,但是直接跟null还是有问题的,还必须第一次不赋值的情况下还要加个?,表示可能为null类型。哈哈哈~习惯一下吧,毕竟这样强制做的好处就是空类型安全。
2. `各种有可能为null的变量使用之前都要加?

actionBack?.onAddAction()

哈哈哈,其实真的很棒的,体验一下吧。
当然如果真的确定一定以及肯定,你可以加在使用之前加!!表示该变量一定不为null
3. switch没了。变成了when

when(x){
    1 -> {}
    2 -> {}
    3 -> {}
    else ->{}
}

这就是最简单的用法,的确去掉那个以前可恶的case 用起来还是很爽的 ,没有多余的代码,也不用break。甚至可以用任意表达式(不只是常量哦)作为分之条件,是不是非常爽哈。当然,这么强大的都可以取代if了。
4. 匿名表达式

valueAnimator.addUpdateListener { animation ->

        }

上面貌似有用到哈,这是最简单的,一参的。和java中的匿名内部类那样,我们直接就可以这么简单粗暴的使用~当然你可以直接选择自动补全

valueAnimator.addListener(object : Animator.AnimatorListener {
            override fun onAnimationRepeat(animation: Animator?) {
            }

            override fun onAnimationEnd(animation: Animator?) {
                actionBack?.onResult(amount)
            }

            override fun onAnimationCancel(animation: Animator?) {
            }

            override fun onAnimationStart(animation: Animator?) {
            }

        })

这边还有一个就是这个鬼~刚开始其实我自动补全啊 ,什么的折腾了一会,就是报错,不知道这边语法要怎么走~然后最后发现竟然有多个需要实现的方法的时候,要这么做~其实我对Kotlin的语法了解也不多,哈哈哈。
5. java中的get真的没了
比如之前你在java中想在Fragment中获取当前Activity,直接通过getActivity()来获取是吧,然后现在就变成了这样activity,直接可以通过activity这个变量来获取~studio也会直接提示,你用自动补全按回车后的结果也是这样哦~
【Kotlin】一个有趣的商品数量加减交互控件(商详页或者购物车商品数量的加减)
6. in关键词
之前比如说你要判断x是否在1到10之间,就要这样做if(x>1&&x<10)但是貌似现在这么写,studio直接会提示你要你改成这样if(in 1..10)哈哈哈 貌似挺棒的,的确简洁。

项目地址

https://github.com/Blincheng/BlinChengDemos

总结

最后吧,提前祝大家周末愉快喽~公司本周周五开始到周日有上海亚米星球美食节,吃货们可以去哈~我也建议大家可以去尝试用Kotlin写一些Demo,这样或许比你直接看Kotlin的文档,以为看一遍就能上手用的快得多,高效得多吧!之前我有尝试直接去看文档,真的。。。那真的枯燥无味,但是你想想,如果你用Kotlin去开发一点东西,然后自然而然就慢慢的熟悉掌握Kotlin的用法了呢!哈哈