You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS - 基于Autolayout约束实现弹性动画,无需表视图/滚动视图可行吗?

只用两个UIView实现拖动弹性显示下方视图的方案

当然可以!完全不需要依赖UITableView或者UIScrollView,咱们用两个普通UIView加上手势识别和Core Animation就能搞定这个弹性拖动效果。下面是具体的实现思路和代码示例:

核心思路

  • 把粉色视图作为底层容器,白色视图放在它上方(初始时白色视图完全覆盖粉色视图)
  • 给白色视图添加UIPanGestureRecognizer监听拖动手势
  • 根据拖动偏移量实时调整白色视图的位置,同时处理松手后的弹性回弹动画
  • 限制拖动边界,避免白色视图被拖得过低或过度向上暴露粉色视图

Swift 实现示例

import UIKit

class ViewController: UIViewController {
    // 底层粉色视图
    private let pinkView: UIView = {
        let view = UIView()
        view.backgroundColor = .systemPink
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    // 上层可拖动白色视图
    private let whiteView: UIView = {
        let view = UIView()
        view.backgroundColor = .white
        view.translatesAutoresizingMaskIntoConstraints = false
        // 可选:添加顶部圆角模拟列表视觉效果
        view.layer.cornerRadius = 16
        view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
        view.clipsToBounds = true
        // 可选:添加阴影增强层次感
        view.layer.shadowColor = UIColor.black.cgColor
        view.layer.shadowOpacity = 0.1
        view.layer.shadowOffset = CGSize(width: 0, height: -2)
        view.layer.shadowRadius = 8
        return view
    }()
    
    // 记录拖动开始时白色视图的初始Y位置
    private var initialViewY: CGFloat = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupViewHierarchy()
        setupPanGesture()
    }
    
    private func setupViewHierarchy() {
        view.addSubview(pinkView)
        view.addSubview(whiteView)
        
        NSLayoutConstraint.activate([
            // 粉色视图铺满整个父视图
            pinkView.topAnchor.constraint(equalTo: view.topAnchor),
            pinkView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            pinkView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            pinkView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            
            // 白色视图初始完全覆盖屏幕
            whiteView.topAnchor.constraint(equalTo: view.topAnchor),
            whiteView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            whiteView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            whiteView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
    
    private func setupPanGesture() {
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        whiteView.addGestureRecognizer(panGesture)
    }
    
    @objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: view)
        
        switch gesture.state {
        case .began:
            // 记录拖动起始位置
            initialViewY = whiteView.frame.origin.y
        case .changed:
            // 计算新的顶部位置:初始位置 + 拖动Y偏移(向上拖动时translation.y为正)
            let newTopPosition = initialViewY + translation.y
            // 限制拖动范围:顶部最低为0(不露出屏幕外),最高不超过屏幕高度的1/3(可自定义)
            let clampedTop = max(0, min(newTopPosition, view.bounds.height / 3))
            // 更新白色视图位置
            whiteView.frame = CGRect(x: 0, y: clampedTop, width: view.bounds.width, height: view.bounds.height)
        case .ended, .cancelled:
            // 松手后触发弹性回弹动画,模拟系统滚动视图的弹性效果
            UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: .curveEaseOut) {
                self.whiteView.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
            }
        default:
            break
        }
    }
}

关键细节说明

  • 手势处理逻辑:通过UIPanGestureRecognizer的translation(in:)获取实时拖动偏移,在.changed状态动态更新视图位置,同时做边界限制避免过度拖动。
  • 弹性动画实现:松手后使用UIView.animate的弹簧参数(usingSpringWithDamping和initialSpringVelocity),还原和UITableView一致的自然弹性回弹效果。
  • 视觉优化:给白色视图添加圆角和阴影,能让整体效果更接近列表的视觉体验。

可选扩展

如果需要实现“拖动到一定距离后停留”的效果,可以在.ended状态判断总拖动偏移量,超过阈值就停留在目标位置,否则回弹:

// 在.ended/.cancelled分支替换原代码
let totalOffset = whiteView.frame.origin.y - initialViewY
if totalOffset > view.bounds.height / 6 {
    // 停留到半露出位置
    UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.6) {
        self.whiteView.frame = CGRect(x: 0, y: self.view.bounds.height / 3, width: self.view.bounds.width, height: self.view.bounds.height)
    }
} else {
    // 回弹到初始位置
    UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: .curveEaseOut) {
        self.whiteView.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
    }
}

内容的提问来源于stack exchange,提问作者Saif

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 07:50:59