修改自定义UIPageControl:用UIBezierPath绘制未选中圆点
自定义PageControl实现圆形未选中指示器及滚动过渡效果
当前代码通过圆角矩形绘制指示器,导致过渡时无法保持圆形。要实现需求,核心是改用圆形绘制逻辑,同时调整过渡动画为两个圆形的缩放渐变,而非拉伸矩形。
以下是修改后的完整代码:
public class PageControl: UIPageControl { // MARK: - Private Properties private var selectedIndex: Int = 0 private var remainingDecimal: CGFloat = 0 private var selectedColor: UIColor = .clear { didSet { reset() } } private var unselectedColor: UIColor = .clear { didSet { reset() } } // MARK: - Public Properties public var dotRadius: CGFloat = 3 { didSet { reset() } } // 移除dotHeight,因为圆形只需要radius控制大小 public var dotSpacings: CGFloat = 7 { didSet { reset() } } public override var numberOfPages: Int { didSet { reset() } } public override var currentPage: Int { didSet { reset() } } public override var pageIndicatorTintColor: UIColor? { get { .clear } set { unselectedColor = newValue ?? .clear } } public override var currentPageIndicatorTintColor: UIColor? { get { .clear } set { selectedColor = newValue ?? .clear } } // MARK: - Public Functions // swiftlint:disable all public override func draw(_ rect: CGRect) { guard numberOfPages > 0 else { return } let dotDiameter = dotRadius * 2 let totalDotWidth = dotDiameter + dotSpacings // 绘制所有静态未选中圆点 for index in 0..<numberOfPages { if index != selectedIndex && index != selectedIndex + 1 { let centerX = CGFloat(index) * totalDotWidth + dotRadius let centerY = rect.midY let circleRect = CGRect(x: centerX - dotRadius, y: centerY - dotRadius, width: dotDiameter, height: dotDiameter) unselectedColor.setFill() UIBezierPath(ovalIn: circleRect).fill() } } // 处理过渡动画的两个圆点 let selectedCenterX = CGFloat(selectedIndex) * totalDotWidth + dotRadius let nextCenterX = CGFloat(selectedIndex + 1) * totalDotWidth + dotRadius let centerY = rect.midY // 当前选中点:随滚动逐渐缩小 let selectedScale = 1 - remainingDecimal let selectedSize = dotDiameter * selectedScale let selectedRect = CGRect(x: selectedCenterX - selectedSize/2, y: centerY - selectedSize/2, width: selectedSize, height: selectedSize) let selectedMixColor = between(selectedColor, and: unselectedColor, percentage: remainingDecimal) selectedMixColor.setFill() UIBezierPath(ovalIn: selectedRect).fill() // 下一个点:随滚动逐渐放大 if selectedIndex + 1 < numberOfPages { let nextScale = remainingDecimal let nextSize = dotDiameter * nextScale let nextRect = CGRect(x: nextCenterX - nextSize/2, y: centerY - nextSize/2, width: nextSize, height: nextSize) let nextMixColor = between(unselectedColor, and: selectedColor, percentage: remainingDecimal) nextMixColor.setFill() UIBezierPath(ovalIn: nextRect).fill() } } // swiftlint:enable all public override var intrinsicContentSize: CGSize { let dotDiameter = dotRadius * 2 guard numberOfPages > 0 else { return .zero } // 修正尺寸计算:(n个点的宽度) + (n-1个间距的宽度) let totalWidth = CGFloat(numberOfPages - 1) * dotSpacings + CGFloat(numberOfPages) * dotDiameter return .init(width: totalWidth, height: dotDiameter) } public func setOffset(_ offset: CGFloat, width: CGFloat) { // 限制索引范围,防止越界 selectedIndex = max(0, min(Int(offset / width), numberOfPages - 1)) remainingDecimal = offset / width - CGFloat(selectedIndex) setNeedsDisplay() } public func scrollViewDidScroll(_ scrollView: UIScrollView) { let width = scrollView.bounds.width let offset = scrollView.contentOffset.x let pageIndex = Int(offset / width) % numberOfPages let newOffset = offset.truncatingRemainder(dividingBy: width * CGFloat(numberOfPages)) setOffset(newOffset, width: width) currentPage = pageIndex setNeedsDisplay() } // MARK: - Private Functions private func reset() { self.invalidateIntrinsicContentSize() self.setNeedsDisplay() } private func between(_ color1: UIColor, and color2: UIColor, percentage: CGFloat) -> UIColor { let percentage = max(min(percentage, 1), 0) switch percentage { case 0: return color1 case 1: return color2 default: var (red1, green1, blue1, alpha1): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0) var (red2, green2, blue2, alpha2): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0) guard color1.getRed(&red1, green: &green1, blue: &blue1, alpha: &alpha1) else { return color1 } guard color2.getRed(&red2, green: &green2, blue: &blue2, alpha: &alpha2) else { return color2 } return UIColor(red: red1 + (red2 - red1) * percentage, green: green1 + (green2 - green1) * percentage, blue: blue1 + (blue2 - blue1) * percentage, alpha: alpha1 + (alpha2 - alpha1) * percentage) } } }
修改说明
- 移除多余属性:删掉了
dotHeight,因为圆形尺寸仅由dotRadius控制,避免参数冗余 - 静态圆点绘制:所有未参与过渡的指示器直接用
ovalIn绘制,确保始终是完美圆形 - 过渡动画优化:滚动时拆分出两个独立的圆形动画:当前选中点逐渐缩小,下一个点逐渐放大,两者始终保持圆形形态,同时伴随颜色渐变
- 修正尺寸计算:调整
intrinsicContentSize的计算逻辑,避免多余留白,保证布局紧凑 - 边界安全处理:在
setOffset中限制selectedIndex的范围,防止索引越界导致的绘制异常
内容的提问来源于stack exchange,提问作者Arohi Khan
相关产品推荐
相关产品推荐

