电商项目购物车简单分析

/ 0

前几天写了个购物车demo,期间有几个朋友找我讨论这个问题,我这里重新分析下购物车的简单原理吧。文章底部有demo链接,本文只会简单分析下实现过程,而不太多涉及代码部分。

shoppingCart

加入购物车抛物效果

很多电商app里都有这个效果,其实也就是在点击了加入购物车按钮后的一个将商品抛到购物车里的动画效果。因为商品图片是在tableViewCell里的,所以我们要想办法将商品图片传递给控制器,来完成这一系列的抛物动画。我们可以使用block或者delegate,看个人喜好。我这里使用delegate,在点击购买按钮后,就将商品图片传递给控制器。核心代码:

    @objc private func didTappedAddCartButton(button: UIButton) {
        
        // 已经购买
        goodModel!.alreadyAddShoppingCart = true
        
        // 已经点击的就禁用
        button.enabled = !goodModel!.alreadyAddShoppingCart
        
        // 通知代理对象,去处理后续操作
        delegate?.goodListCell(self, iconView: iconView)
    }

然后在控制器实现代理方法,成为cell的代理对象。在这个代理方法里,获取到被点击的模型,加入我们定义的一个临时购物车模型数组里,并转换商品图片的坐标系,参照与当前控制器的view,并调用startAnimation方法开始执行动画。

    func goodListCell(cell: JFGoodListCell, iconView: UIImageView) {
        
        guard let indexPath = tableView.indexPathForCell(cell) else {
            return
        }
        
        // 获取当前模型,添加到购物车模型数组
        let model = goodArray[indexPath.row]
        addGoodArray.append(model)
        
        // 重新计算iconView的frame,并开启动画
        var rect = tableView .rectForRowAtIndexPath(indexPath)
        rect.origin.y -= tableView.contentOffset.y
        var headRect = iconView.frame
        headRect.origin.y = rect.origin.y + headRect.origin.y - 64
        startAnimation(headRect, iconView: iconView)
    }

在startAnimation方法里,我根据商品图片创建了一个layer来完成动画,并绘制动画的抛物路径,这里我使用UIBezierPath,给定一个起点为商品图片的位置,给定一个终点为购物车位置,再给定一个贝塞尔曲线的控制点让路径有一个弧度。然后开启组动画groupAnimation,将要执行的一系列动画添加到我们自定义的layer上。

    private func startAnimation(rect: CGRect, iconView: UIImageView) {
        if layer == nil {
            layer = CALayer()
            layer?.contents = iconView.layer.contents
            layer?.contentsGravity = kCAGravityResizeAspectFill
            layer?.bounds = rect
            layer?.cornerRadius = CGRectGetHeight(layer!.bounds) * 0.5
            layer?.masksToBounds = true
            layer?.position = CGPoint(x: iconView.center.x, y: CGRectGetMinY(rect) + 96)
            UIApplication.sharedApplication().keyWindow?.layer.addSublayer(layer!)
            
            path = UIBezierPath()
            path!.moveToPoint(layer!.position)
            path!.addQuadCurveToPoint(CGPoint(x: SCREEN_WIDTH - 25, y: 35), controlPoint: CGPoint(x: SCREEN_WIDTH * 0.5, y: rect.origin.y - 80))
        }
        
        // 组动画
        groupAnimation()
    }

在groupAnimation方法里,禁用tableView的交互。然后我添加了一个抛物路径的帧动画和放大、缩小layer的基础动画,并将这三个动画加入一个组动画里执行。最后实现代理方法animationDidStop,在动画停止后做一些操作。

    private func groupAnimation() {
        
        // 开始动画禁用tableview交互
        tableView.userInteractionEnabled = false
        
        // 帧动画
        let animation = CAKeyframeAnimation(keyPath: "position")
        animation.path = path!.CGPath
        animation.rotationMode = kCAAnimationRotateAuto
        
        // 放大动画
        let bigAnimation = CABasicAnimation(keyPath: "transform.scale")
        bigAnimation.duration = 0.5
        bigAnimation.fromValue = 1
        bigAnimation.toValue = 2
        bigAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
        
        // 缩小动画
        let smallAnimation = CABasicAnimation(keyPath: "transform.scale")
        smallAnimation.beginTime = 0.5
        smallAnimation.duration = 1.5
        smallAnimation.fromValue = 2
        smallAnimation.toValue = 0.3
        smallAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
        
        // 组动画
        let groupAnimation = CAAnimationGroup()
        groupAnimation.animations = [animation, bigAnimation, smallAnimation]
        groupAnimation.duration = 2
        groupAnimation.removedOnCompletion = false
        groupAnimation.fillMode = kCAFillModeForwards
        groupAnimation.delegate = self
        layer?.addAnimation(groupAnimation, forKey: "groupAnimation")
    }

在animationDidStop方法里,在组动画完成后开启tableView的交互。移除我们用来做动画自定义layer图层,并实现购物车的简单抖动动画和商品数量渐出效果。至此,加入购物车的动画效果也就完成了。

    override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
        
        // 如果动画是我们的组动画,才开始一些操作
        if anim == layer?.animationForKey("groupAnimation") {
            
            // 开启交互
            tableView.userInteractionEnabled = true
            
            // 隐藏图层
            layer?.removeAllAnimations()
            layer?.removeFromSuperlayer()
            layer = nil
            
            // 如果商品数大于0,显示购物车里的商品数量
            if self.addGoodArray.count > 0 {
                addCountLabel.hidden = false
            }
            
            // 商品数量渐出
            let goodCountAnimation = CATransition()
            goodCountAnimation.duration = 0.25
            addCountLabel.text = "\(self.addGoodArray.count)"
            addCountLabel.layer.addAnimation(goodCountAnimation, forKey: nil)
            
            // 购物车抖动
            let cartAnimation = CABasicAnimation(keyPath: "transform.translation.y")
            cartAnimation.duration = 0.25
            cartAnimation.fromValue = -5
            cartAnimation.toValue = 5
            cartAnimation.autoreverses = true
            cartButton.layer.addAnimation(cartAnimation, forKey: nil)
        }
    }

购物车商品增减

实现同一个商品数量增加、商品单选、多选、反选的原理其实非常简单,只要记住我们操作的永远是商品模型,界面上显示的内容都是根据模型里的数据来展示的,增加、减少商品数量也是修改模型里对应的属性,选中、不选中也是在模型里定义一个是否被选中的布尔类型属性。有了这些理解,我们实现起来就比较方便了,需要注意我们修改cell中的数据,也就是修改当前cell对于的模型时,我们需要使用delegate将这个操作传递给控制器来实现。这样管理起来非常的方便,具体看demo,我注释已经写得非常多了,可能单选、多选、反选逻辑和你的需求不匹配,但只要知道了这个原理都是可以随意修改的。

demo采用swift语言、纯代码编写ui、autolayout技术布局,如果对swift语言还不熟悉的朋友,可以参考注释来看,然后自己使用熟悉的语言实践。

demo地址:https://github.com/6ag/shoppingCart