使滚动视图内容重新居中 Swift
Recenter the scrollview content Swift
当用户当前位置超出屏幕框架时,我正在尝试重新居中滚动视图视图并移动框架。目前我有一个 PDF,我正在显示用户的当前位置,我正在计算框架和滚动视图 zoomScale 以在 PDF 视图上显示当前位置。我已经实现了这个功能。它工作完美,当用户移动时我已经用相同的逻辑绘制路径,但是当用户移动并离开屏幕时我被困在最后一点意味着从移动屏幕隐藏然后我们需要重新居中当前位置。
第一个密码:-
代码:-
override func viewDidLoad() {
super.viewDidLoad()
self.initPdfView()
self.initCurrentLocation()
}
func initPdfView() {
do {
let paths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
if let path = paths.first {
let fileURL = URL(fileURLWithPath: path).appendingPathComponent("MapBox")
let document = try PDFDocument.init(at: fileURL)
pdfController?.page = try document.page(0)
pdfController?.scrollDelegates = self
pdfController?.scrollView.layoutSubviews()
}
} catch {
print(error.localizedDescription)
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let visibleRect = CGRect.init(x: sender.contentOffset.x, y: sender.contentOffset.y, width: sender.contentSize.width*sender.zoomScale, height: sender.contentSize.height*sender.zoomScale)
self.visibleScrollViewRect = visibleRect
self.zooomLevel = sender.zoomScale
}
func initCurrentLocation() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let locValue: CLLocationCoordinate2D = manager.location?.coordinate else { return }
self.currentLocation = locValue
}
截图:-
在第一个屏幕截图中,我显示了用户的当前位置。
First Screenshot
在第二个屏幕截图中,我在用户当前位置的帮助下绘制路径。
Second Screenshot
在第三个屏幕截图中,当用户移动并离开屏幕时意味着从移动屏幕隐藏。
Third Screenshot
第二个密码:-
func initMapDataUserView() {
guard let mapInfoJson = decodeMapInfo(with: "MapBoxUrl") else {
return
}
let position = CGPoint.init(x: mapInfoJson.rasterXYsize.first!, y: mapInfoJson.rasterXYsize.last!)
let pointerVal: UnsafePointer<Int8>? = NSString(string: mapInfoJson.projection).utf8String
let decoder = GeoDecode()
decoder.fetchPdfCoordinateBounds(with: position, projection: pointerVal, initialTransform: mapInfoJson.geotransform) { coordinate, error in
if let error = error {
debugPrint(error)
} else {
guard let coordinate = coordinate else {
return
}
self.coordinatesUserCurrentLocation = coordinate
self.initCurrentLocation()
}
}
}
func initPdfView() {
do {
let paths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
if let path = paths.first {
let fileURL = URL(fileURLWithPath: path).appendingPathComponent("MapBoxUrl")
let document = try PDFDocument.init(at: fileURL)
viewPDFController?.page = try document.page(0)
viewPDFController?.scrollDelegates = self
viewPDFController?.scrollView.layoutSubviews()
}
} catch {
print(error.localizedDescription)
}
}
func decodeMapInfo(with value: String) -> MapInfoJson? {
do {
guard let valueData = value.data(using: .utf8) else {
return nil
}
let decodedResult = try JSONDecoder().decode(MapInfoJson.self, from: valueData)
return decodedResult
} catch {
print("error: ", error)
}
return nil
}
extension MapPreviewViewController: scrollViewActions {
func scrollViewScroll(_ sender: UIScrollView) {
let visibleRect = CGRect.init(x: sender.contentOffset.x, y: sender.contentOffset.y, width: sender.contentSize.width*sender.zoomScale, height: sender.contentSize.height*sender.zoomScale)
self.visibleScrollViewRectUserScreen = visibleRect
self.zooomLevelScrollView = sender.zoomScale
if coordinatesUserCurrentLocation != nil {
updateMarkerVisiblityOnPdfView()
}
}
}
extension MapPreviewViewController: CLLocationManagerDelegate {
func initCurrentLocation() {
locationManagerUserTest.delegate = self
locationManagerUserTest.desiredAccuracy = kCLLocationAccuracyBest
locationManagerUserTest.requestAlwaysAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManagerUserTest.desiredAccuracy = kCLLocationAccuracyBest
locationManagerUserTest.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let locValue: CLLocationCoordinate2D = manager.location?.coordinate else { return }
self.currentLocationUser = locValue
updateMarkerVisiblityOnPdfView()
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
}
func updateMarkerVisiblityOnPdfView() {
guard let locValue: CLLocationCoordinate2D = self.currentLocationUser else { return }
guard let coordinates = coordinatesUserCurrentLocation else { return }
let yFactor = (locValue.longitude - coordinates.minY) / (coordinates.maxY - coordinates.minY)
let xFactor = (coordinates.maxX - locValue.latitude) / (coordinates.maxX - coordinates.minX)
var positionX: Double = 0.0
var positionY: Double = 0.0
positionX = (yFactor*Double(visibleScrollViewRectUserScreen!.size.width))/Double(self.zooomLevelScrollView!)
positionY = (xFactor*Double(visibleScrollViewRectUserScreen!.size.height))/Double(self.zooomLevelScrollView!)
if visibleScrollViewRectUserScreen!.size.width < 1.0 {
positionX = (yFactor*Double(18))*Double(self.zooomLevelScrollView!)
positionY = (xFactor*Double(18))*Double(self.zooomLevelScrollView!)
}
var indexOfExistingImageView: Int?
for index in 0..<viewPDFController!.scrollView.subviews.count {
if let imageview = viewPDFController!.scrollView.subviews[index] as? UIImageView {
if imageview.image == currentmarkerImagView.image {
indexOfExistingImageView = index
}
}
}
self.currentmarkerImagView.center = .init(x: positionX, y: positionY)
self.viewPDFController!.scrollView.addSubview(currentmarkerImagView)
self.viewPDFController!.scrollView.bringSubviewToFront(currentmarkerImagView)
}
}
public protocol scrollViewActions {
func scrollViewScroll(_ sender: UIScrollView)
}
public class PdfViewViewController: UIViewController {
public var scrollView: UIScrollView!
public var overlayView: UIView!
public var contentView: UIView!
public var scrollDelegates: scrollViewActions?
public override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
scrollView.contentInsetAdjustmentBehavior = .never
}
}
extension PdfViewViewController: UIScrollViewDelegate {
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollDelegates?.scrollViewScroll(scrollView)
}
public func scrollViewDidZoom(_ scrollView: UIScrollView) {
scrollDelegates?.scrollViewScroll(scrollView)
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
scrollDelegates?.scrollViewScroll(scrollView)
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
}
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
}
public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
}
}
更新的屏幕截图:
在第一个屏幕截图中,我显示了用户的当前位置。
First Screenshot
在第二个屏幕截图中,当用户移动并离开屏幕时意味着从移动屏幕隐藏。
Second Screenshot
问题:谁能解释一下当用户移动并离开屏幕时,如何使当前位置重新居中或移动滚动视图框架。
如有任何帮助,我们将不胜感激。
提前致谢。
嗯,显然,我们无法创建新项目,粘贴您发布的代码,然后 运行 它。
所以...希望这会有所帮助。
滚动视图的 .bounds
是其 .contentSize
.
的 可见矩形
所以,如果我们举这个例子:
- 创建一个 400x600“mapView”(作为
viewForZooming
)
- 在
origin: x: 240, y: 400
添加一个 30x30 的“标记”子视图
- 使用 200 x 300 帧的滚动视图(黄色背景)
- 将 mapView 的所有 4 个边都约束到滚动视图的
.contentLayoutGuide
从 1.0 缩放开始看起来像这样(当然,滚动视图框架之外的所有内容都将被隐藏):
滚动视图将具有:
ContentSize: (400.0, 600.0)
Bounds: (0.0, 0.0, 200.0, 300.0)
如果我们一直滚动到右下角,它将看起来像这样:
与:
ContentSize: (400.0, 600.0)
Bounds: (200.0, 300.0, 200.0, 300.0)
如果我们放大到 2.0 缩放比例,我们得到:
ContentSize: (800.0, 1200.0)
Bounds: (0.0, 0.0, 200.0, 300.0)
ContentSize: (800.0, 1200.0)
Bounds: (600.0, 900.0, 200.0, 300.0)
如果我们放大到 3.0 缩放比例,我们得到:
ContentSize: (1200.0, 1800.0)
Bounds: (0.0, 0.0, 200.0, 300.0)
ContentSize: (1200.0, 1800.0)
Bounds: (1000.0, 1500.0, 200.0, 300.0)
如果我们将 out 缩放到 0.5 缩放比例:
ContentSize: (200.0, 300.0)
Bounds: (0.0, 0.0, 200.0, 300.0)
第一个任务是找出“标记”是否可见...如果是,我们不需要做任何事情。如果它 不 可见(滚动到框架外),我们希望它居中。
所以,如果我们像这样滚动:
我们可以说:
let r = marker.frame
let isInside = scrollView.bounds.contains(r)
在这种情况下,isInside
将是 true
。
但是,如果标记 在 框架之外,如下所示:
我们要定义一个 CGRect
与滚动视图的边界相同的宽度和高度,以标记的中心为中心:
我们可以调用:
scrollView.scrollRectToVisible(r, animated: true)
当然,如果我们的标记视图靠近边缘,像这样:
这是最接近中心的位置。
虽然还不够...
标记视图的框架将始终是它自己的框架 - 它会在滚动视图的缩放比例发生变化时进行缩放。所以,我们需要考虑到这一点:
let r = marker.frame.applying(CGAffineTransform(scaleX: self.scrollView.zoomScale, y: self.scrollView.zoomScale))
let isInside = scrollView.bounds.contains(r)
这是一个完整的示例演示...我们创建了滚动视图大小两倍的“地图视图”,最小缩放:0.5,最大缩放:3.0,排列“标记”模式并使用两个按钮“如果需要突出显示并居中":
注意:仅示例代码 - 并非旨在“生产就绪”:
class CenterInScrollVC: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let v = UIScrollView()
return v
}()
// can be any type of view
// using a "Dashed Outline" so we can see its edges
let mapView: DashView = {
let v = DashView()
v.backgroundColor = UIColor(white: 0.9, alpha: 0.5)
v.color = .blue
v.style = .border
return v
}()
var mapMarkers: [UIView] = []
var markerIndex: Int = 0
// let's make the markers 40x40
let markerSize: CGFloat = 40.0
// percentage of one-half of marker that must be visible to NOT screll to center
// 1.0 == entire marker must be visible
// 0.5 == up to 1/4 of marker may be out of view
// <= 0.0 == only check that the Center of the marker is in view
// can be set to > 1.0 to require entire marker Plus some "padding"
let pctVisible: CGFloat = 1.0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
// a button to center the current marker (if needed)
let btnA: UIButton = {
let v = UIButton()
v.backgroundColor = .systemRed
v.setTitleColor(.white, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
v.setTitle("Center Current if Needed", for: [])
v.addTarget(self, action: #selector(btnATap(_:)), for: .touchUpInside)
return v
}()
// a button to select the next marker, center if needed
let btnB: UIButton = {
let v = UIButton()
v.backgroundColor = .systemRed
v.setTitleColor(.white, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
v.setTitle("Go To Marker - 2", for: [])
v.addTarget(self, action: #selector(btnBTap(_:)), for: .touchUpInside)
return v
}()
// add a view with a "+" marker to show the center of the scroll view
let centerView: DashView = {
let v = DashView()
v.backgroundColor = .clear
v.color = UIColor(red: 0.95, green: 0.2, blue: 1.0, alpha: 0.5)
v.style = .centerMarker
v.isUserInteractionEnabled = false
return v
}()
[btnA, btnB, mapView, scrollView, centerView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
[btnA, btnB, scrollView, centerView].forEach { v in
view.addSubview(v)
}
scrollView.addSubview(mapView)
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// buttons at the top
btnA.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 20.0),
btnA.widthAnchor.constraint(equalTo: safeG.widthAnchor, multiplier: 0.7),
btnA.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
btnB.topAnchor.constraint(equalTo: btnA.bottomAnchor, constant: 20.0),
btnB.widthAnchor.constraint(equalTo: btnA.widthAnchor),
btnB.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
// let's inset the scroll view to make it easier to distinguish
scrollView.topAnchor.constraint(equalTo: btnB.bottomAnchor, constant: 40.0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 40.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -40.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -40.0),
// overlay "center lines" view
centerView.topAnchor.constraint(equalTo: scrollView.topAnchor),
centerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
centerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
centerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
// mapView Top/Leading/Trailing/Bottom to scroll view's CONTENT GUIDE
mapView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
mapView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
mapView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
mapView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
// let's make the mapView twice as wide and tall as the scroll view
mapView.widthAnchor.constraint(equalTo: frameG.widthAnchor, multiplier: 2.0),
mapView.heightAnchor.constraint(equalTo: frameG.heightAnchor, multiplier: 2.0),
])
// some example locations for the Markers
let pcts: [[CGFloat]] = [
[0.50, 0.50],
[0.25, 0.50],
[0.50, 0.25],
[0.75, 0.50],
[0.50, 0.75],
[0.10, 0.15],
[0.90, 0.15],
[0.90, 0.85],
[0.10, 0.85],
]
for (i, p) in pcts.enumerated() {
let v = UILabel()
v.text = "\(i + 1)"
v.textAlignment = .center
v.textColor = .yellow
v.backgroundColor = .systemBlue
v.font = .systemFont(ofSize: 15.0, weight: .bold)
v.translatesAutoresizingMaskIntoConstraints = false
mapMarkers.append(v)
mapView.addSubview(v)
v.widthAnchor.constraint(equalToConstant: markerSize).isActive = true
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
NSLayoutConstraint(item: v, attribute: .centerX, relatedBy: .equal, toItem: mapView, attribute: .trailing, multiplier: p[0], constant: 0.0).isActive = true
NSLayoutConstraint(item: v, attribute: .centerY, relatedBy: .equal, toItem: mapView, attribute: .bottom, multiplier: p[1], constant: 0.0).isActive = true
}
scrollView.minimumZoomScale = 0.5
scrollView.maximumZoomScale = 3.0
scrollView.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// let's start with the scroll view zoomed out
scrollView.zoomScale = scrollView.minimumZoomScale
// highlight and center (if needed) the 1st marker
markerIndex = 0
let marker = mapMarkers[markerIndex % mapMarkers.count]
highlightMarkerAndCenterIfNeeded(marker, animated: true)
}
@objc func btnATap(_ sender: Any?) {
// to easily test "center if not visible" without changing the "current marker"
let marker = mapMarkers[markerIndex % mapMarkers.count]
highlightMarkerAndCenterIfNeeded(marker, animated: true)
}
@objc func btnBTap(_ sender: Any?) {
// increment index to the next marker
markerIndex += 1
let marker = mapMarkers[markerIndex % mapMarkers.count]
// center if needed
highlightMarkerAndCenterIfNeeded(marker, animated: true)
// update button title
if let b = sender as? UIButton, let m = mapMarkers[(markerIndex + 1) % mapMarkers.count] as? UILabel, let t = m.text {
b.setTitle("Go To Marker - \(t)", for: [])
}
}
func highlightMarkerAndCenterIfNeeded(_ marker: UIView, animated: Bool) {
// "un-highlight" all markers
mapMarkers.forEach { v in
v.backgroundColor = .systemBlue
}
// "highlight" the new marker
marker.backgroundColor = .systemGreen
// get the marker frame, scaled by zoom scale
var r = marker.frame.applying(CGAffineTransform(scaleX: self.scrollView.zoomScale, y: self.scrollView.zoomScale))
// inset the rect if we allow less-than-full marker visible
if pctVisible > 0.0 {
let iw: CGFloat = (1.0 - pctVisible) * r.width * 0.5
let ih: CGFloat = (1.0 - pctVisible) * r.height * 0.5
r = r.insetBy(dx: iw, dy: ih)
}
var isInside: Bool = true
if pctVisible <= 0.0 {
// check center point only
isInside = self.scrollView.bounds.contains(CGPoint(x: r.midX, y: r.midY))
} else {
// check the rect
isInside = self.scrollView.bounds.contains(r)
}
// if the marker rect (or point) IS inside the scroll view
// we don't do anything
// if it's NOT inside the scroll view
// center it
if !isInside {
// create a rect using scroll view's bounds centered on marker's center
let w: CGFloat = self.scrollView.bounds.width
let h: CGFloat = self.scrollView.bounds.height
r = CGRect(x: r.midX, y: r.midY, width: w, height: h).offsetBy(dx: -w * 0.5, dy: -h * 0.5)
if animated {
// let's slow down the animation a little
UIView.animate(withDuration: 0.75, delay: 0.0, options: [.curveEaseInOut], animations: {
self.scrollView.scrollRectToVisible(r, animated: false)
}, completion: nil)
} else {
self.scrollView.scrollRectToVisible(r, animated: false)
}
}
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return mapView
}
}
Edit -- 如代码注释中所述,任何视图都可用于 viewForZooming
,但这里是我使用的 DashView
的代码:
class DashView: UIView {
// border or
// vertical and horizontal center lines or
// two lines forming a + in the center
enum Style: Int {
case border
case centerLines
case centerMarker
}
public var style: Style = .border {
didSet {
setNeedsLayout()
}
}
// solid or dashed
public var solid: Bool = false
// line color
public var color: UIColor = .yellow {
didSet {
dashLayer.strokeColor = color.cgColor
}
}
private let dashLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
layer.addSublayer(dashLayer)
dashLayer.strokeColor = color.cgColor
dashLayer.fillColor = UIColor.clear.cgColor
dashLayer.lineWidth = 2
}
override func layoutSubviews() {
super.layoutSubviews()
var bez = UIBezierPath()
switch style {
case .border:
bez = UIBezierPath(rect: bounds)
dashLayer.lineDashPattern = [10, 10]
case .centerLines:
bez.move(to: CGPoint(x: bounds.midX, y: bounds.minY))
bez.addLine(to: CGPoint(x: bounds.midX, y: bounds.maxY))
bez.move(to: CGPoint(x: bounds.minX, y: bounds.midY))
bez.addLine(to: CGPoint(x: bounds.maxX, y: bounds.midY))
dashLayer.lineDashPattern = [10, 10]
case .centerMarker:
bez.move(to: CGPoint(x: bounds.midX, y: bounds.midY - 40.0))
bez.addLine(to: CGPoint(x: bounds.midX, y: bounds.midY + 40.0))
bez.move(to: CGPoint(x: bounds.midX - 40.0, y: bounds.midY))
bez.addLine(to: CGPoint(x: bounds.midX + 40.0, y: bounds.midY))
dashLayer.lineDashPattern = []
}
if solid {
dashLayer.lineDashPattern = []
}
dashLayer.path = bez.cgPath
}
}
我找到了检查和设置 pdf 滚动视图中心的解决方案。
Code:-
func getCurrentLocMakerOutSideFrameOrNot() {
var visibleRect = CGRect.init(x: 0, y: 0, width: 0, height: 0)
visibleRect.origin = scrollView.contentOffset
visibleRect.size = scrollView.bounds.size
if !visibleRect.intersects(currentmarkerImagView.frame) {
print("Outer View")
guard let locValue: CLLocationCoordinate2D = self.currentLocation else { return }
guard let coordinates = coordinates else { return }
let yFactor = (locValue.longitude - coordinates.minY) / (coordinates.maxY - coordinates.minY)
let xFactor = (coordinates.maxX - locValue.latitude) / (coordinates.maxX - coordinates.minX)
var positionX: Double = 0.0
var positionY: Double = 0.0
positionX = (yFactor*Double(visibleScrollViewRectView!.size.width))/Double(self.zooomLevelView!)
positionY = (xFactor*Double(visibleScrollViewRectView!.size.height))/Double(self.zooomLevelView!)
if visibleScrollViewRectView!.size.width < 1.0 {
positionX = (yFactor*Double(18))*Double(self.zooomLevelView!)
positionY = (xFactor*Double(18))*Double(self.zooomLevelView!)
}
let centerPoint = CGPoint(x: positionX-190, y: positionY-295)
UIView.animate(withDuration: 2.5, animations: {
scrollView.setContentOffset(centerPoint, animated: false)
scrollView.setZoomScale(self.pdfController!.scrollView.zoomScale/2, animated: false)
})
}
}
当用户当前位置超出屏幕框架时,我正在尝试重新居中滚动视图视图并移动框架。目前我有一个 PDF,我正在显示用户的当前位置,我正在计算框架和滚动视图 zoomScale 以在 PDF 视图上显示当前位置。我已经实现了这个功能。它工作完美,当用户移动时我已经用相同的逻辑绘制路径,但是当用户移动并离开屏幕时我被困在最后一点意味着从移动屏幕隐藏然后我们需要重新居中当前位置。
第一个密码:-
代码:-
override func viewDidLoad() {
super.viewDidLoad()
self.initPdfView()
self.initCurrentLocation()
}
func initPdfView() {
do {
let paths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
if let path = paths.first {
let fileURL = URL(fileURLWithPath: path).appendingPathComponent("MapBox")
let document = try PDFDocument.init(at: fileURL)
pdfController?.page = try document.page(0)
pdfController?.scrollDelegates = self
pdfController?.scrollView.layoutSubviews()
}
} catch {
print(error.localizedDescription)
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let visibleRect = CGRect.init(x: sender.contentOffset.x, y: sender.contentOffset.y, width: sender.contentSize.width*sender.zoomScale, height: sender.contentSize.height*sender.zoomScale)
self.visibleScrollViewRect = visibleRect
self.zooomLevel = sender.zoomScale
}
func initCurrentLocation() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let locValue: CLLocationCoordinate2D = manager.location?.coordinate else { return }
self.currentLocation = locValue
}
截图:-
在第一个屏幕截图中,我显示了用户的当前位置。
First Screenshot
在第二个屏幕截图中,我在用户当前位置的帮助下绘制路径。
Second Screenshot
在第三个屏幕截图中,当用户移动并离开屏幕时意味着从移动屏幕隐藏。
Third Screenshot
第二个密码:-
func initMapDataUserView() {
guard let mapInfoJson = decodeMapInfo(with: "MapBoxUrl") else {
return
}
let position = CGPoint.init(x: mapInfoJson.rasterXYsize.first!, y: mapInfoJson.rasterXYsize.last!)
let pointerVal: UnsafePointer<Int8>? = NSString(string: mapInfoJson.projection).utf8String
let decoder = GeoDecode()
decoder.fetchPdfCoordinateBounds(with: position, projection: pointerVal, initialTransform: mapInfoJson.geotransform) { coordinate, error in
if let error = error {
debugPrint(error)
} else {
guard let coordinate = coordinate else {
return
}
self.coordinatesUserCurrentLocation = coordinate
self.initCurrentLocation()
}
}
}
func initPdfView() {
do {
let paths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
if let path = paths.first {
let fileURL = URL(fileURLWithPath: path).appendingPathComponent("MapBoxUrl")
let document = try PDFDocument.init(at: fileURL)
viewPDFController?.page = try document.page(0)
viewPDFController?.scrollDelegates = self
viewPDFController?.scrollView.layoutSubviews()
}
} catch {
print(error.localizedDescription)
}
}
func decodeMapInfo(with value: String) -> MapInfoJson? {
do {
guard let valueData = value.data(using: .utf8) else {
return nil
}
let decodedResult = try JSONDecoder().decode(MapInfoJson.self, from: valueData)
return decodedResult
} catch {
print("error: ", error)
}
return nil
}
extension MapPreviewViewController: scrollViewActions {
func scrollViewScroll(_ sender: UIScrollView) {
let visibleRect = CGRect.init(x: sender.contentOffset.x, y: sender.contentOffset.y, width: sender.contentSize.width*sender.zoomScale, height: sender.contentSize.height*sender.zoomScale)
self.visibleScrollViewRectUserScreen = visibleRect
self.zooomLevelScrollView = sender.zoomScale
if coordinatesUserCurrentLocation != nil {
updateMarkerVisiblityOnPdfView()
}
}
}
extension MapPreviewViewController: CLLocationManagerDelegate {
func initCurrentLocation() {
locationManagerUserTest.delegate = self
locationManagerUserTest.desiredAccuracy = kCLLocationAccuracyBest
locationManagerUserTest.requestAlwaysAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManagerUserTest.desiredAccuracy = kCLLocationAccuracyBest
locationManagerUserTest.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let locValue: CLLocationCoordinate2D = manager.location?.coordinate else { return }
self.currentLocationUser = locValue
updateMarkerVisiblityOnPdfView()
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
}
func updateMarkerVisiblityOnPdfView() {
guard let locValue: CLLocationCoordinate2D = self.currentLocationUser else { return }
guard let coordinates = coordinatesUserCurrentLocation else { return }
let yFactor = (locValue.longitude - coordinates.minY) / (coordinates.maxY - coordinates.minY)
let xFactor = (coordinates.maxX - locValue.latitude) / (coordinates.maxX - coordinates.minX)
var positionX: Double = 0.0
var positionY: Double = 0.0
positionX = (yFactor*Double(visibleScrollViewRectUserScreen!.size.width))/Double(self.zooomLevelScrollView!)
positionY = (xFactor*Double(visibleScrollViewRectUserScreen!.size.height))/Double(self.zooomLevelScrollView!)
if visibleScrollViewRectUserScreen!.size.width < 1.0 {
positionX = (yFactor*Double(18))*Double(self.zooomLevelScrollView!)
positionY = (xFactor*Double(18))*Double(self.zooomLevelScrollView!)
}
var indexOfExistingImageView: Int?
for index in 0..<viewPDFController!.scrollView.subviews.count {
if let imageview = viewPDFController!.scrollView.subviews[index] as? UIImageView {
if imageview.image == currentmarkerImagView.image {
indexOfExistingImageView = index
}
}
}
self.currentmarkerImagView.center = .init(x: positionX, y: positionY)
self.viewPDFController!.scrollView.addSubview(currentmarkerImagView)
self.viewPDFController!.scrollView.bringSubviewToFront(currentmarkerImagView)
}
}
public protocol scrollViewActions {
func scrollViewScroll(_ sender: UIScrollView)
}
public class PdfViewViewController: UIViewController {
public var scrollView: UIScrollView!
public var overlayView: UIView!
public var contentView: UIView!
public var scrollDelegates: scrollViewActions?
public override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
scrollView.contentInsetAdjustmentBehavior = .never
}
}
extension PdfViewViewController: UIScrollViewDelegate {
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollDelegates?.scrollViewScroll(scrollView)
}
public func scrollViewDidZoom(_ scrollView: UIScrollView) {
scrollDelegates?.scrollViewScroll(scrollView)
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
scrollDelegates?.scrollViewScroll(scrollView)
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
}
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
}
public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
}
}
更新的屏幕截图:
在第一个屏幕截图中,我显示了用户的当前位置。
First Screenshot
在第二个屏幕截图中,当用户移动并离开屏幕时意味着从移动屏幕隐藏。
Second Screenshot
问题:谁能解释一下当用户移动并离开屏幕时,如何使当前位置重新居中或移动滚动视图框架。
如有任何帮助,我们将不胜感激。
提前致谢。
嗯,显然,我们无法创建新项目,粘贴您发布的代码,然后 运行 它。
所以...希望这会有所帮助。
滚动视图的 .bounds
是其 .contentSize
.
所以,如果我们举这个例子:
- 创建一个 400x600“mapView”(作为
viewForZooming
) - 在
origin: x: 240, y: 400
添加一个 30x30 的“标记”子视图
- 使用 200 x 300 帧的滚动视图(黄色背景)
- 将 mapView 的所有 4 个边都约束到滚动视图的
.contentLayoutGuide
从 1.0 缩放开始看起来像这样(当然,滚动视图框架之外的所有内容都将被隐藏):
滚动视图将具有:
ContentSize: (400.0, 600.0)
Bounds: (0.0, 0.0, 200.0, 300.0)
如果我们一直滚动到右下角,它将看起来像这样:
与:
ContentSize: (400.0, 600.0)
Bounds: (200.0, 300.0, 200.0, 300.0)
如果我们放大到 2.0 缩放比例,我们得到:
ContentSize: (800.0, 1200.0)
Bounds: (0.0, 0.0, 200.0, 300.0)
ContentSize: (800.0, 1200.0)
Bounds: (600.0, 900.0, 200.0, 300.0)
如果我们放大到 3.0 缩放比例,我们得到:
ContentSize: (1200.0, 1800.0)
Bounds: (0.0, 0.0, 200.0, 300.0)
ContentSize: (1200.0, 1800.0)
Bounds: (1000.0, 1500.0, 200.0, 300.0)
如果我们将 out 缩放到 0.5 缩放比例:
ContentSize: (200.0, 300.0)
Bounds: (0.0, 0.0, 200.0, 300.0)
第一个任务是找出“标记”是否可见...如果是,我们不需要做任何事情。如果它 不 可见(滚动到框架外),我们希望它居中。
所以,如果我们像这样滚动:
我们可以说:
let r = marker.frame
let isInside = scrollView.bounds.contains(r)
在这种情况下,isInside
将是 true
。
但是,如果标记 在 框架之外,如下所示:
我们要定义一个 CGRect
与滚动视图的边界相同的宽度和高度,以标记的中心为中心:
我们可以调用:
scrollView.scrollRectToVisible(r, animated: true)
当然,如果我们的标记视图靠近边缘,像这样:
这是最接近中心的位置。
虽然还不够...
标记视图的框架将始终是它自己的框架 - 它会在滚动视图的缩放比例发生变化时进行缩放。所以,我们需要考虑到这一点:
let r = marker.frame.applying(CGAffineTransform(scaleX: self.scrollView.zoomScale, y: self.scrollView.zoomScale))
let isInside = scrollView.bounds.contains(r)
这是一个完整的示例演示...我们创建了滚动视图大小两倍的“地图视图”,最小缩放:0.5,最大缩放:3.0,排列“标记”模式并使用两个按钮“如果需要突出显示并居中":
注意:仅示例代码 - 并非旨在“生产就绪”:
class CenterInScrollVC: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let v = UIScrollView()
return v
}()
// can be any type of view
// using a "Dashed Outline" so we can see its edges
let mapView: DashView = {
let v = DashView()
v.backgroundColor = UIColor(white: 0.9, alpha: 0.5)
v.color = .blue
v.style = .border
return v
}()
var mapMarkers: [UIView] = []
var markerIndex: Int = 0
// let's make the markers 40x40
let markerSize: CGFloat = 40.0
// percentage of one-half of marker that must be visible to NOT screll to center
// 1.0 == entire marker must be visible
// 0.5 == up to 1/4 of marker may be out of view
// <= 0.0 == only check that the Center of the marker is in view
// can be set to > 1.0 to require entire marker Plus some "padding"
let pctVisible: CGFloat = 1.0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
// a button to center the current marker (if needed)
let btnA: UIButton = {
let v = UIButton()
v.backgroundColor = .systemRed
v.setTitleColor(.white, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
v.setTitle("Center Current if Needed", for: [])
v.addTarget(self, action: #selector(btnATap(_:)), for: .touchUpInside)
return v
}()
// a button to select the next marker, center if needed
let btnB: UIButton = {
let v = UIButton()
v.backgroundColor = .systemRed
v.setTitleColor(.white, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
v.setTitle("Go To Marker - 2", for: [])
v.addTarget(self, action: #selector(btnBTap(_:)), for: .touchUpInside)
return v
}()
// add a view with a "+" marker to show the center of the scroll view
let centerView: DashView = {
let v = DashView()
v.backgroundColor = .clear
v.color = UIColor(red: 0.95, green: 0.2, blue: 1.0, alpha: 0.5)
v.style = .centerMarker
v.isUserInteractionEnabled = false
return v
}()
[btnA, btnB, mapView, scrollView, centerView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
[btnA, btnB, scrollView, centerView].forEach { v in
view.addSubview(v)
}
scrollView.addSubview(mapView)
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// buttons at the top
btnA.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 20.0),
btnA.widthAnchor.constraint(equalTo: safeG.widthAnchor, multiplier: 0.7),
btnA.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
btnB.topAnchor.constraint(equalTo: btnA.bottomAnchor, constant: 20.0),
btnB.widthAnchor.constraint(equalTo: btnA.widthAnchor),
btnB.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
// let's inset the scroll view to make it easier to distinguish
scrollView.topAnchor.constraint(equalTo: btnB.bottomAnchor, constant: 40.0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 40.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -40.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -40.0),
// overlay "center lines" view
centerView.topAnchor.constraint(equalTo: scrollView.topAnchor),
centerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
centerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
centerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
// mapView Top/Leading/Trailing/Bottom to scroll view's CONTENT GUIDE
mapView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
mapView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
mapView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
mapView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
// let's make the mapView twice as wide and tall as the scroll view
mapView.widthAnchor.constraint(equalTo: frameG.widthAnchor, multiplier: 2.0),
mapView.heightAnchor.constraint(equalTo: frameG.heightAnchor, multiplier: 2.0),
])
// some example locations for the Markers
let pcts: [[CGFloat]] = [
[0.50, 0.50],
[0.25, 0.50],
[0.50, 0.25],
[0.75, 0.50],
[0.50, 0.75],
[0.10, 0.15],
[0.90, 0.15],
[0.90, 0.85],
[0.10, 0.85],
]
for (i, p) in pcts.enumerated() {
let v = UILabel()
v.text = "\(i + 1)"
v.textAlignment = .center
v.textColor = .yellow
v.backgroundColor = .systemBlue
v.font = .systemFont(ofSize: 15.0, weight: .bold)
v.translatesAutoresizingMaskIntoConstraints = false
mapMarkers.append(v)
mapView.addSubview(v)
v.widthAnchor.constraint(equalToConstant: markerSize).isActive = true
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
NSLayoutConstraint(item: v, attribute: .centerX, relatedBy: .equal, toItem: mapView, attribute: .trailing, multiplier: p[0], constant: 0.0).isActive = true
NSLayoutConstraint(item: v, attribute: .centerY, relatedBy: .equal, toItem: mapView, attribute: .bottom, multiplier: p[1], constant: 0.0).isActive = true
}
scrollView.minimumZoomScale = 0.5
scrollView.maximumZoomScale = 3.0
scrollView.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// let's start with the scroll view zoomed out
scrollView.zoomScale = scrollView.minimumZoomScale
// highlight and center (if needed) the 1st marker
markerIndex = 0
let marker = mapMarkers[markerIndex % mapMarkers.count]
highlightMarkerAndCenterIfNeeded(marker, animated: true)
}
@objc func btnATap(_ sender: Any?) {
// to easily test "center if not visible" without changing the "current marker"
let marker = mapMarkers[markerIndex % mapMarkers.count]
highlightMarkerAndCenterIfNeeded(marker, animated: true)
}
@objc func btnBTap(_ sender: Any?) {
// increment index to the next marker
markerIndex += 1
let marker = mapMarkers[markerIndex % mapMarkers.count]
// center if needed
highlightMarkerAndCenterIfNeeded(marker, animated: true)
// update button title
if let b = sender as? UIButton, let m = mapMarkers[(markerIndex + 1) % mapMarkers.count] as? UILabel, let t = m.text {
b.setTitle("Go To Marker - \(t)", for: [])
}
}
func highlightMarkerAndCenterIfNeeded(_ marker: UIView, animated: Bool) {
// "un-highlight" all markers
mapMarkers.forEach { v in
v.backgroundColor = .systemBlue
}
// "highlight" the new marker
marker.backgroundColor = .systemGreen
// get the marker frame, scaled by zoom scale
var r = marker.frame.applying(CGAffineTransform(scaleX: self.scrollView.zoomScale, y: self.scrollView.zoomScale))
// inset the rect if we allow less-than-full marker visible
if pctVisible > 0.0 {
let iw: CGFloat = (1.0 - pctVisible) * r.width * 0.5
let ih: CGFloat = (1.0 - pctVisible) * r.height * 0.5
r = r.insetBy(dx: iw, dy: ih)
}
var isInside: Bool = true
if pctVisible <= 0.0 {
// check center point only
isInside = self.scrollView.bounds.contains(CGPoint(x: r.midX, y: r.midY))
} else {
// check the rect
isInside = self.scrollView.bounds.contains(r)
}
// if the marker rect (or point) IS inside the scroll view
// we don't do anything
// if it's NOT inside the scroll view
// center it
if !isInside {
// create a rect using scroll view's bounds centered on marker's center
let w: CGFloat = self.scrollView.bounds.width
let h: CGFloat = self.scrollView.bounds.height
r = CGRect(x: r.midX, y: r.midY, width: w, height: h).offsetBy(dx: -w * 0.5, dy: -h * 0.5)
if animated {
// let's slow down the animation a little
UIView.animate(withDuration: 0.75, delay: 0.0, options: [.curveEaseInOut], animations: {
self.scrollView.scrollRectToVisible(r, animated: false)
}, completion: nil)
} else {
self.scrollView.scrollRectToVisible(r, animated: false)
}
}
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return mapView
}
}
Edit -- 如代码注释中所述,任何视图都可用于 viewForZooming
,但这里是我使用的 DashView
的代码:
class DashView: UIView {
// border or
// vertical and horizontal center lines or
// two lines forming a + in the center
enum Style: Int {
case border
case centerLines
case centerMarker
}
public var style: Style = .border {
didSet {
setNeedsLayout()
}
}
// solid or dashed
public var solid: Bool = false
// line color
public var color: UIColor = .yellow {
didSet {
dashLayer.strokeColor = color.cgColor
}
}
private let dashLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
layer.addSublayer(dashLayer)
dashLayer.strokeColor = color.cgColor
dashLayer.fillColor = UIColor.clear.cgColor
dashLayer.lineWidth = 2
}
override func layoutSubviews() {
super.layoutSubviews()
var bez = UIBezierPath()
switch style {
case .border:
bez = UIBezierPath(rect: bounds)
dashLayer.lineDashPattern = [10, 10]
case .centerLines:
bez.move(to: CGPoint(x: bounds.midX, y: bounds.minY))
bez.addLine(to: CGPoint(x: bounds.midX, y: bounds.maxY))
bez.move(to: CGPoint(x: bounds.minX, y: bounds.midY))
bez.addLine(to: CGPoint(x: bounds.maxX, y: bounds.midY))
dashLayer.lineDashPattern = [10, 10]
case .centerMarker:
bez.move(to: CGPoint(x: bounds.midX, y: bounds.midY - 40.0))
bez.addLine(to: CGPoint(x: bounds.midX, y: bounds.midY + 40.0))
bez.move(to: CGPoint(x: bounds.midX - 40.0, y: bounds.midY))
bez.addLine(to: CGPoint(x: bounds.midX + 40.0, y: bounds.midY))
dashLayer.lineDashPattern = []
}
if solid {
dashLayer.lineDashPattern = []
}
dashLayer.path = bez.cgPath
}
}
我找到了检查和设置 pdf 滚动视图中心的解决方案。
Code:-
func getCurrentLocMakerOutSideFrameOrNot() {
var visibleRect = CGRect.init(x: 0, y: 0, width: 0, height: 0)
visibleRect.origin = scrollView.contentOffset
visibleRect.size = scrollView.bounds.size
if !visibleRect.intersects(currentmarkerImagView.frame) {
print("Outer View")
guard let locValue: CLLocationCoordinate2D = self.currentLocation else { return }
guard let coordinates = coordinates else { return }
let yFactor = (locValue.longitude - coordinates.minY) / (coordinates.maxY - coordinates.minY)
let xFactor = (coordinates.maxX - locValue.latitude) / (coordinates.maxX - coordinates.minX)
var positionX: Double = 0.0
var positionY: Double = 0.0
positionX = (yFactor*Double(visibleScrollViewRectView!.size.width))/Double(self.zooomLevelView!)
positionY = (xFactor*Double(visibleScrollViewRectView!.size.height))/Double(self.zooomLevelView!)
if visibleScrollViewRectView!.size.width < 1.0 {
positionX = (yFactor*Double(18))*Double(self.zooomLevelView!)
positionY = (xFactor*Double(18))*Double(self.zooomLevelView!)
}
let centerPoint = CGPoint(x: positionX-190, y: positionY-295)
UIView.animate(withDuration: 2.5, animations: {
scrollView.setContentOffset(centerPoint, animated: false)
scrollView.setZoomScale(self.pdfController!.scrollView.zoomScale/2, animated: false)
})
}
}