【Kotlin】一个有趣的商品数量加减交互控件(商详页或者购物车商品数量的加减)
前言
自从5月18号那天Google宣布Kotlin成为Android第一开发语言的时候,Kotlin就火了,然后再6月Kotlin就直接挤入编程语言排行前50,好吧,既然Google都这么做了,那我们也是不是应该适当的熟悉熟悉呢?所以,我想,接下来的我的博客应该都会尝试用Kotlin来开发,然后再说说遇到的问题和自己的感受吧。
既然本次开发使用Kotlin的,那么如果不熟悉或者不会在Android studio上用Kotlin开发的,可以先去看看我之前(去年)的博文
【Android】使用Kotlin在Android Studio上开发App
先看看今天我们要实现的效果,自己感受下吧,可能这边效果不是很棒
引出BlinChengDemos
然后既然要用Kotlin了,那么我们就新建一个全新的项目吧,然后我想把我之前的所有东西都整合起来变成一个大的Demo,直接共享到github上供大家参考。然后该全新的项目当然是用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也会直接提示,你用自动补全按回车后的结果也是这样哦~
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的用法了呢!哈哈