搞事情之 PJRulerPickerView 组件开发总结

栏目: IOS · 发布时间: 5年前

内容简介:搞事情系列文章主要是为了继续延续自己的 “T” 字形战略所做,同时也代表着毕设相关内容的学习总结。本文是实现项目中一个选择器组件引发的思考。有人说过“一个好的产品通常会在一些细节的处理上取胜”,这一点非常好的在我身上进行了验证。在去年完成了一版选择器的设计后(详情见此文章),现如今进行了第二版的实现。看到设计图后,我不禁感叹,设计小哥的脑洞真是大的可以,完全抛弃了常规的选择器设计。
搞事情之 PJRulerPickerView 组件开发总结

搞事情系列文章主要是为了继续延续自己的 “T” 字形战略所做,同时也代表着毕设相关内容的学习总结。本文是实现项目中一个选择器组件引发的思考。

前言

有人说过“一个好的产品通常会在一些细节的处理上取胜”,这一点非常好的在我身上进行了验证。在去年完成了一版选择器的设计后(详情见此文章),现如今进行了第二版的实现。

看到设计图后,我不禁感叹,设计小哥的脑洞真是大的可以,完全抛弃了常规的选择器设计。

搞事情之 PJRulerPickerView 组件开发总结

与 UI 确认了动效后,脑海里立马浮现了“我不要自己写!”的想法,但很快又意识到估计不会有这种开源组件可以用。总之给自己埋下了这是整个项目中最难实现动效之一的种子。

调研

不出所料,在 github 上尝试搜索过了 pickerswpierslider 等众多与选择器相关的关键词后均无果,甚至还尝试改造了 collectionView 中间放大的组件,但一番操作后,发现实在是不堪入目。

经历过这次的改造后,发现 collectionView 中间视图放大的效果是基于动态改变出现 cellscale 属性去做的,开始萌生了干脆自己写一个得了。

思考

盯着设计图看了好久,反复琢磨动效。最后自己总结出以下几种实现思路:

  • 使用 UICollectionView 集合余弦定理做 scale 变换,可以随便找一个开源组件做二次开发(时间最短)。
  • 使用 UICollectionView ,每个 cell 都是一样大小,中间部分做“放大镜”效果,把整个 collectionView 做 3D 转换变为从带深度的一个滚轮,每次滚动都只是在修改 x 轴上的内容,z 轴和 y 轴不动(效果最好)。
  • 使用 UIScrollView 做“轮播图”效果,所有东西都需要自己来(实现最简单)。

其实我大部分的时间都花在了第一种方案上,因为实际动效跟第一种方案完全一致,只不过 cell 特别小就是了。但前面也说过了在尝试过二次修改几个开源组件后,发现效果实在是惨不忍睹,遂放弃;第二种方案是自己独创的,也是因为动效特别像一个垂直于屏幕的滚轮,但做过 3D 变换的同学也是知道需要调整很多参数,实在是得不偿失。

最好用了一个最简单直接方法,用 UIScrollView 硬造。

搞事情之 PJRulerPickerView 组件开发总结

实现

第一步

首先需要把素材都准备好,我很快的写出了把所有子视图排布在 scrollView 中的代码。

搞事情之 PJRulerPickerView 组件开发总结
private func initView() {
    let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: pj_width, height: pj_height))
    scrollView.showsHorizontalScrollIndicator = false
    scrollView.showsVerticalScrollIndicator = false
    addSubview(scrollView)
    var finalW: CGFloat = 0
    for index in 0..<pickCount {
        let inner = 10
        let sv = UIView(frame: CGRect(x: 10 + index * inner, y: Int(scrollView.pj_height / 2), width: 1, height: 4))
        sv.backgroundColor = .lightGray
        scrollView.addSubview(sv)

        if index == pickCount - 1 {
            finalW = sv.right
        }
    }
    scrollView.contentSize = CGSize(width: finalW, height: 0)
}
复制代码

第二步

需要把靠近屏幕中间的几个视图按规则进行拉高。花费了一些时间来寻找把中间视图拉高的参数,调整了一下。

搞事情之 PJRulerPickerView 组件开发总结
private func initView() {
        
    let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: pj_width, height: pj_height))
    scrollView.showsHorizontalScrollIndicator = false
    scrollView.showsVerticalScrollIndicator = false 
    addSubview(scrollView)
    
    var finalW: CGFloat = 0
    for index in 0..<pickCount {
        
        // 子视图之间的间距
        let inner = 10
        // sv 为每个子视图
        let sv = UIView(frame: CGRect(x: 10 + index * inner, y: Int(scrollView.pj_height / 2), width: 1, height: 4))
        sv.backgroundColor = .lightGray
        sv.tag = index + 100
        scrollView.addSubview(sv)
        
        // 当前子视图是否在中心区域范围内
        if abs(sv.centerX - centerX) < 5 {
            
            sv.pj_height = 18
            sv.pj_width = 2
            sv.backgroundColor = .black
            // 先赋值给中心视图
            centerView = sv
            
        } else if abs(sv.centerX - centerX) < 16 {
            
            sv.pj_height = 14
            sv.pj_width = 1
            
        } else if abs(sv.centerX - centerX) < 26 {
            
            sv.pj_height = 8
            sv.pj_width = 1
            
        } else {
            
            sv.pj_height = 4
            sv.pj_width = 1
            
        }
        
        sv.y = (scrollView.pj_height - sv.pj_height) * 0.5
        
        if index == pickCount - 1 {
            
            finalW = sv.right
            
        }
    }
    
    scrollView.contentSize = CGSize(width: finalW, height: 0)
}
复制代码

第三步

滚动时需要实时计算中间区域视图的高度。有了初始化视图时的判断条件,直接拿来用即可,只不过需要加上 scrollView 滑动的 x 轴偏移量。

搞事情之 PJRulerPickerView 组件开发总结
extension PJRulerPickerView: UIScrollViewDelegate {
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        let offSetX = scrollView.contentOffset.x
        let _ = scrollView.subviews.filter {
            
            if abs($0.centerX - offSetX - centerX) < 5 {
                
                $0.pj_height = 18
                $0.pj_width = 2
                $0.backgroundColor = .black
                
            } else if abs($0.centerX - offSetX  - centerX) < 16 {
                
                $0.pj_height = 14
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            } else if abs($0.centerX - offSetX - centerX) < 26 {
                
                $0.pj_height = 8
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            } else {
                
                $0.pj_height = 4
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            }

            $0.y = (scrollView.pj_height - $0.pj_height) * 0.5
            return true   
        }
    }
}
复制代码

第四步

做到这基本上就简单的完成了需求,一点都不复杂有没有!!!真是不知道为什么要花费大半天的时间去找开源库,去做二次开发。

在向 UI 确定动效的过程中,被告知左右两边的视图不能被“拖没”,意思就是关闭“弹簧效果”,使用 scrollView.bounces = false 属性进行关闭。

此时发现允许用户拨动 100 次,但因为“弹簧效果”的关闭导致了可滚动的内容变少了。思考了一下后,运用了一些简单的数学计算让 scrollView 多渲染了头部和尾部占据的滚动内容。

private func initView() {
    
    let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: pj_width, height: pj_height))
    addSubview(scrollView)
    scrollView.delegate = self
    scrollView.showsHorizontalScrollIndicator = false
    scrollView.showsVerticalScrollIndicator = false
    scrollView.bounces = false

    // 从屏幕左边到屏幕中心占据的个数
    // 10.5 为每一个子视图的宽度 + 左边距,多加 1 是把第一个渲染出来的中心视图也加上
    startIndex = (Int(ceil(centerX / 10.5)) + 1)
    // 总共需要渲染的子视图加上头尾占据的个数
    pickCount += startIndex * 2
    
    var finalW: CGFloat = 0
    
    for index in 0..<pickCount {
        
        // 子视图之间的间距
        let inner = 10
        // sv 为每个子视图
        let sv = UIView(frame: CGRect(x: 10 + index * inner, y: Int(scrollView.pj_height / 2), width: 1, height: 4))
        sv.backgroundColor = .lightGray
        scrollView.addSubview(sv)
        
        // 当前子视图是否在中心区域范围内
        if abs(sv.centerX - centerX) < 5 {
            
            sv.pj_height = 18
            sv.pj_width = 2
            sv.backgroundColor = .black
            // 先赋值给中心视图
            centerView = sv
            
        } else if abs(sv.centerX - centerX) < 16 {
            
            sv.pj_height = 14
            sv.pj_width = 1
            
        } else if abs(sv.centerX - centerX) < 26 {
            
            sv.pj_height = 8
            sv.pj_width = 1
            
        } else {
            
            sv.pj_height = 4
            sv.pj_width = 1
            
        }
        
        sv.y = (scrollView.pj_height - sv.pj_height) * 0.5
        
        if index == pickCount - 1 {
            
            finalW = sv.right
            
        }
    }
    
    scrollView.contentSize = CGSize(width: finalW, height: 0)
}
复制代码

第五步

现在基本上解决了 UI 问题,最后只需要把用户拨动的次数暴露出去即可。思考了一会后,得出这么个结论:计算用户当前拨动选择器的次数,实际上就是计算中间视图“变黑”了几次。想明白后,我很快的写下了代码:

private func initView() {
    
    let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: pj_width, height: pj_height))
    addSubview(scrollView)
    scrollView.delegate = self
    scrollView.showsHorizontalScrollIndicator = false
    scrollView.showsVerticalScrollIndicator = false
    scrollView.bounces = false

    // 从屏幕左边到屏幕中心占据的个数
    startIndex = (Int(ceil(centerX / 10.5)) + 1)
    // 总共需要渲染的子视图加上头尾占据的个数
    pickCount += startIndex * 2
    
    var finalW: CGFloat = 0
    
    for index in 0..<pickCount {
        
        // 子视图之间的间距
        let inner = 10
        // sv 为每个子视图
        let sv = UIView(frame: CGRect(x: 10 + index * inner, y: Int(scrollView.pj_height / 2), width: 1, height: 4))
        sv.backgroundColor = .lightGray
        sv.tag = index + 100
        scrollView.addSubview(sv)
        
        // 当前子视图是否在中心区域范围内
        if abs(sv.centerX - centerX) < 5 {
            
            sv.pj_height = 18
            sv.pj_width = 2
            sv.backgroundColor = .black
            // 先赋值给中心视图
            centerView = sv
            
        } else if abs(sv.centerX - centerX) < 16 {
            
            sv.pj_height = 14
            sv.pj_width = 1
            
        } else if abs(sv.centerX - centerX) < 26 {
            
            sv.pj_height = 8
            sv.pj_width = 1
            
        } else {
            
            sv.pj_height = 4
            sv.pj_width = 1
            
        }
        
        sv.y = (scrollView.pj_height - sv.pj_height) * 0.5
        
        if index == pickCount - 1 {
            
            finalW = sv.right
            
        }
    }
    
    scrollView.contentSize = CGSize(width: finalW, height: 0)
}

extension PJRulerPickerView: UIScrollViewDelegate {
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        let offSetX = scrollView.contentOffset.x
        let _ = scrollView.subviews.filter {
            
            if abs($0.centerX - offSetX - centerX) < 5 {
                
                $0.pj_height = 18
                $0.pj_width = 2
                $0.backgroundColor = .black
                
                // 如果本次的中心视图不是上一次的中心视图,说明中心视图进行了替换
                if centerView.tag != $0.tag {
                    
                    centerView = $0
                    // 在此处可以进行计算拨动次数
                }
            } else if abs($0.centerX - offSetX  - centerX) < 16 {
                
                $0.pj_height = 14
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
            } else if abs($0.centerX - offSetX - centerX) < 26 {
                
                $0.pj_height = 8
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
            } else {
                
                $0.pj_height = 4
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
            }

            $0.y = (scrollView.pj_height - $0.pj_height) * 0.5
            return true
        }
    }
}
复制代码

我使用了一个中间变量去作为中间视图的引用,并在创建子视图时给其加上 tag 用于标记。思考了一下后,受到前几次的思考影响,导致了计算用户拨动过几次的方法也不假思索的做了一些数学计算,最后我是这么做的:

extension PJRulerPickerView: UIScrollViewDelegate {
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        let offSetX = scrollView.contentOffset.x
        let _ = scrollView.subviews.filter {
            
            if abs($0.centerX - offSetX - centerX) < 5 {
                
                $0.pj_height = 18
                $0.pj_width = 2
                $0.backgroundColor = .black
                
                // 如果本次的中心视图不是上一次的中心视图
                if centerView.tag != $0.tag {
                    
                    PJTapic.select()
                    centerView = $0
                    
                    // 用户拨动的次数
                    print(Int(ceil($0.centerX / 10.5)) - startIndex)
                }
                
            } else if abs($0.centerX - offSetX  - centerX) < 16 {
                
                $0.pj_height = 14
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            } else if abs($0.centerX - offSetX - centerX) < 26 {
                
                $0.pj_height = 8
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            } else {
                
                $0.pj_height = 4
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            }

            $0.y = (scrollView.pj_height - $0.pj_height) * 0.5
            return true
        }
    }
}
复制代码

在刚才写这篇文章时,我发现了一个特别傻的地方,我都已经把每个子视图所代表的位置记录进了 tag 中,为社么还要重新计算一遍当前中间视图的位置?意识到这个问题后,还修改了一些其它地方,最终 PJRulerPickerView 的全部代码如下:

//
//  PJRulerPicker.swift
//  PIGPEN
//
//  Created by PJHubs on 2019/5/16.
//  Copyright © 2019 PJHubs. All rights reserved.
//

import UIKit

class PJRulerPickerView: UIView {
    
    /// 获取拨动次数
    var moved: ((Int) -> Void)?
    /// 需要拨动的次数
    var pickCount  = 0
    // 中心视图
    private var centerView = UIView()
    private var startIndex = 0
    
    override init(frame: CGRect) {
        
        super.init(frame: frame)
    }
    
    required init?(coder aDecoder: NSCoder) {
        
        fatalError("init(coder:) has not been implemented")
        
    }
    
    convenience init(frame: CGRect, pickCount: Int) {
        
        self.init(frame: frame)
        self.pickCount = pickCount
        initView()
        
    }
    
    private func initView() {
        
        let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: pj_width, height: pj_height))
        addSubview(scrollView)
        scrollView.delegate = self
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.showsVerticalScrollIndicator = false
        scrollView.bounces = false

        // 从屏幕左边到屏幕中心占据的个数
        startIndex = (Int(ceil(centerX / 10.5)))
        // 总共需要渲染的子视图加上头尾占据的个数
        pickCount += startIndex * 2 + 1
        
        var finalW: CGFloat = 0
        
        for index in 0..<pickCount {
            
            // 子视图之间的间距
            let inner = 10
            // sv 为每个子视图
            let sv = UIView(frame: CGRect(x: 10 + index * inner, y: Int(scrollView.pj_height / 2), width: 1, height: 4))
            sv.backgroundColor = .lightGray
            sv.tag = index + 100
            scrollView.addSubview(sv)
            
            // 当前子视图是否在中心区域范围内
            if abs(sv.centerX - centerX) < 5 {
                
                sv.pj_height = 18
                sv.pj_width = 2
                sv.backgroundColor = .black
                // 先赋值给中心视图
                centerView = sv
                
            } else if abs(sv.centerX - centerX) < 16 {
                
                sv.pj_height = 14
                sv.pj_width = 1
                
            } else if abs(sv.centerX - centerX) < 26 {
                
                sv.pj_height = 8
                sv.pj_width = 1
                
            } else {
                
                sv.pj_height = 4
                sv.pj_width = 1
                
            }
            
            sv.y = (scrollView.pj_height - sv.pj_height) * 0.5
            
            if index == pickCount - 1 {
                
                finalW = sv.right
                
            }
        }
        
        scrollView.contentSize = CGSize(width: finalW, height: 0)
    }
}

extension PJRulerPickerView: UIScrollViewDelegate {
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        let offSetX = scrollView.contentOffset.x
        let _ = scrollView.subviews.filter {
            
            if abs($0.centerX - offSetX - centerX) < 5 {
                
                $0.pj_height = 18
                $0.pj_width = 2
                $0.backgroundColor = .black
                
                // 如果本次的中心视图不是上一次的中心视图
                if centerView.tag != $0.tag {
                    
                    PJTapic.select()
                    centerView = $0
                    
//                    moved?(Int(ceil($0.centerX / 10.5)) - startIndex)
                    moved?($0.tag - 100 - startIndex)
//                    print($0.tag - 100 - startIndex)
                }
                
            } else if abs($0.centerX - offSetX  - centerX) < 16 {
                
                $0.pj_height = 14
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            } else if abs($0.centerX - offSetX - centerX) < 26 {
                
                $0.pj_height = 8
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            } else {
                
                $0.pj_height = 4
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
                
            }

            $0.y = (scrollView.pj_height - $0.pj_height) * 0.5
            return true
        }
    }
}
复制代码

以上所述就是小编给大家介绍的《搞事情之 PJRulerPickerView 组件开发总结》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

第二次机器革命

第二次机器革命

[美]埃里克·布莱恩约弗森 / 蒋永军 / 中信出版社 / 2014-9 / 59.80

“一本非常鼓舞人心的书!”——托马斯•L•弗里德曼 《世界是平的》作者 一场革命开始了! 在《第二次机器革命》这本书中,埃里克•布莱恩约弗森和安德鲁•麦卡菲——这两位处于数字技术时代最前沿的思想家,向我们阐述了驱动我们经济和生活的发生变革的力量。他们认为,数字技术将会给我们带来难以想象的巨大变革:想象一下令人眩目的个人数字技术产品、一流的基础设施,都将会给我们带来极大的便利。数字技术(......一起来看看 《第二次机器革命》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

SHA 加密
SHA 加密

SHA 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器