// // HomeLocationReportCardView.swift // suixinkan // import SnapKit import UIKit /// 首页位置上报卡片。 final class HomeLocationReportCardView: UIView { var onReportTap: (() -> Void)? private let contentStackView = UIStackView() private let titleStackView = UIStackView() private let titleIconView = UIImageView(image: UIImage(systemName: "arrow.up.square.fill")) private let titleLabel = UILabel() private let addressLabel = UILabel() private let reportButtonContainer = UIView() private let reportButton = UIButton(type: .system) private let reportGradientLayer = CAGradientLayer() override init(frame: CGRect) { super.init(frame: frame) backgroundColor = AppColor.cardBackground layer.cornerRadius = AppRadius.lg layer.borderWidth = 1 layer.borderColor = AppColor.cardOutline.cgColor clipsToBounds = true contentStackView.axis = .vertical contentStackView.alignment = .fill contentStackView.spacing = AppSpacing.xxs titleStackView.axis = .horizontal titleStackView.alignment = .center titleStackView.spacing = AppSpacing.xs titleIconView.tintColor = AppColor.primary titleIconView.contentMode = .scaleAspectFit titleLabel.text = "立即上报" titleLabel.font = .systemFont(ofSize: 24, weight: .bold) titleLabel.textColor = AppColor.textPrimary addressLabel.font = .app(.subtitle) addressLabel.textColor = AppColor.textTertiary addressLabel.numberOfLines = 2 reportGradientLayer.colors = [AppColor.primary.cgColor, AppColor.primaryGradientEnd.cgColor] reportGradientLayer.startPoint = CGPoint(x: 0.5, y: 0) reportGradientLayer.endPoint = CGPoint(x: 0.5, y: 1) reportButtonContainer.layer.insertSublayer(reportGradientLayer, at: 0) reportButtonContainer.layer.cornerRadius = 46 reportButtonContainer.clipsToBounds = true reportButton.backgroundColor = .clear reportButton.tintColor = .white reportButton.setImage(UIImage(systemName: "hand.tap.fill"), for: .normal) reportButton.setPreferredSymbolConfiguration( UIImage.SymbolConfiguration(pointSize: 38, weight: .semibold), forImageIn: .normal ) reportButton.imageView?.contentMode = .scaleAspectFit reportButton.isExclusiveTouch = true reportButton.addTarget(self, action: #selector(reportTouchDown), for: .touchDown) reportButton.addTarget(self, action: #selector(reportTouchUpInside), for: .touchUpInside) reportButton.addTarget(self, action: #selector(reportTouchEnded), for: [.touchUpOutside, .touchCancel, .touchDragExit]) reportButton.accessibilityLabel = "上报位置" titleStackView.addArrangedSubview(titleIconView) titleStackView.addArrangedSubview(titleLabel) contentStackView.addArrangedSubview(titleStackView) contentStackView.addArrangedSubview(addressLabel) addSubview(contentStackView) addSubview(reportButtonContainer) reportButtonContainer.addSubview(reportButton) titleIconView.snp.makeConstraints { make in make.width.height.equalTo(25) } contentStackView.snp.makeConstraints { make in make.leading.equalToSuperview().offset(AppSpacing.md) make.centerY.equalTo(reportButtonContainer) make.trailing.equalTo(reportButtonContainer.snp.leading).offset(-AppSpacing.sm) make.top.greaterThanOrEqualToSuperview().offset(AppSpacing.md) make.bottom.lessThanOrEqualToSuperview().offset(-AppSpacing.md) } reportButtonContainer.snp.makeConstraints { make in make.trailing.equalToSuperview().offset(-AppSpacing.md) make.centerY.equalToSuperview() make.width.height.equalTo(96) } reportButton.snp.makeConstraints { make in make.edges.equalToSuperview() } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() reportGradientLayer.frame = reportButtonContainer.bounds reportGradientLayer.cornerRadius = reportButtonContainer.bounds.width / 2 } func apply(address: String, isReporting: Bool) { addressLabel.text = address reportButton.isEnabled = !isReporting reportButton.alpha = 1 if isReporting { resetReportButtonScale() } reportGradientLayer.colors = isReporting ? [AppColor.controlDisabledStart.cgColor, AppColor.controlDisabledEnd.cgColor] : [AppColor.primary.cgColor, AppColor.primaryGradientEnd.cgColor] } @objc private func reportTouchDown() { animateReportButtonScale(isPressed: true) playCenteredRipple() } @objc private func reportTouchUpInside() { animateReportButtonScale(isPressed: false) onReportTap?() } @objc private func reportTouchEnded() { animateReportButtonScale(isPressed: false) } private func playCenteredRipple() { reportButtonContainer.layer.sublayers? .filter { $0.name == "HomeLocationReportRippleLayer" } .forEach { $0.removeFromSuperlayer() } let diameter = max(reportButtonContainer.bounds.width, reportButtonContainer.bounds.height) * 1.45 let rippleLayer = CALayer() rippleLayer.name = "HomeLocationReportRippleLayer" rippleLayer.backgroundColor = UIColor.white.withAlphaComponent(0.28).cgColor rippleLayer.bounds = CGRect(x: 0, y: 0, width: diameter, height: diameter) rippleLayer.cornerRadius = diameter / 2 rippleLayer.position = CGPoint(x: reportButtonContainer.bounds.midX, y: reportButtonContainer.bounds.midY) rippleLayer.transform = CATransform3DMakeScale(0.18, 0.18, 1) rippleLayer.opacity = 0.32 reportButtonContainer.layer.insertSublayer(rippleLayer, below: reportButton.layer) let scaleAnimation = CABasicAnimation(keyPath: "transform.scale") scaleAnimation.fromValue = 0.18 scaleAnimation.toValue = 1 let opacityAnimation = CABasicAnimation(keyPath: "opacity") opacityAnimation.fromValue = 0.32 opacityAnimation.toValue = 0 let group = CAAnimationGroup() group.animations = [scaleAnimation, opacityAnimation] group.duration = 0.55 group.timingFunction = CAMediaTimingFunction(name: .easeOut) group.isRemovedOnCompletion = false group.fillMode = .forwards CATransaction.begin() CATransaction.setCompletionBlock { [weak rippleLayer] in rippleLayer?.removeFromSuperlayer() } rippleLayer.add(group, forKey: "centeredRipple") CATransaction.commit() } private func animateReportButtonScale(isPressed: Bool) { UIView.animate( withDuration: isPressed ? 0.12 : 0.18, delay: 0, options: [.beginFromCurrentState, .curveEaseOut] ) { self.reportButtonContainer.transform = isPressed ? CGAffineTransform(scaleX: 0.96, y: 0.96) : .identity } } private func resetReportButtonScale() { reportButtonContainer.layer.removeAllAnimations() reportButtonContainer.transform = .identity } }