掌握iOS图片浏览交互:UIScrollView手势与动画

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:在iOS开发中,实现图片浏览的左右上下滑动切换功能是常见的交互设计,适用于图片浏览应用和游戏等。通过使用UIScrollView来设定可滚动区域,并通过手势识别处理用户滑动动作,开发者可以为用户界面添加平滑的图片切换动画效果。文章将介绍UIScrollView的使用、手势识别器的集成、自动布局的应用、图片加载与缓存策略、动画效果的实现以及性能优化技巧,帮助开发者掌握如何构建流畅的图片浏览体验。

1. 使用UIScrollView实现图片滚动切换

概述UIScrollView的作用与应用场景

UIScrollView 是iOS开发中常用的组件,它提供了滚动视图的能力,允许用户通过滑动屏幕来查看超出当前显示区域的内容。这对于图片浏览、文章阅读等场景尤为重要,使得用户可以轻松地浏览大量内容而不必进行繁琐的翻页操作。

基本使用方法和代码示例

要使用 UIScrollView 实现图片滚动切换,首先需要在你的界面中添加一个 UIScrollView 。然后,将一系列图片设置为 UIScrollView 的子视图,每个图片视图都设定为合适的大小,并放置在正确的位置。

// 示例:Swift 代码创建UIScrollView并添加子视图
let scrollView = UIScrollView(frame: self.view.bounds)
self.view.addSubview(scrollView)

let image1 = UIImageView(image: UIImage(named: "image1.jpg"))
// 调整图片位置和大小以适应UIScrollView
image1.frame = CGRect(x: 0, y: 0, width: scrollView.frame.width, height: 200)
scrollView.addSubview(image1)

let image2 = UIImageView(image: UIImage(named: "image2.jpg"))
image2.frame = CGRect(x: 0, y: image1.frame.height, width: scrollView.frame.width, height: 200)
scrollView.addSubview(image2)
// 重复上述步骤添加更多图片

解决图片滚动时的性能问题

在实现图片滚动功能时,如果处理不当,可能会遇到卡顿等性能问题。为了优化滚动性能,可以考虑以下几个方面:

  • 减少滚动视图中子视图的数量 :只展示当前屏幕上可见的图片,并在滚动时动态添加或移除子视图。
  • 优化图片资源 :使用合适大小和格式的图片,避免使用过大的图片资源,以减少内存和处理的负担。
  • 异步加载图片 :在后台线程加载图片,然后在主线程中更新UI。
// 异步加载图片并更新UIScrollView
DispatchQueue.global(qos: .background).async {
    let image = UIImage(named: "image.jpg")
    DispatchQueue.main.async {
        let imageView = UIImageView(image: image)
        // 将imageView添加到scrollView中
    }
}

通过上述方法,可以有效地使用 UIScrollView 来实现图片滚动切换,同时保证应用的流畅性和用户体验。在后续章节中,我们将探讨更多的交互和优化技术。

2. 手势识别实现用户交互

2.1 常见手势识别技术概述

2.1.1 基础手势的定义和使用

手势识别技术在移动设备上的运用日益广泛,它为用户与应用程序之间的交互提供了直观且自然的方式。基础手势通常包括轻触、长按、滑动、拖动和双击等。在iOS开发中, UIKit 框架提供了 UIGestureRecognizer 类,以及其子类如 UITapGestureRecognizer UIPinchGestureRecognizer 等,用于识别和处理这些常见手势。

UITapGestureRecognizer 可以识别单击或多次轻触的手势,是实现点击功能的常用类。 UIPinchGestureRecognizer 用于处理捏合动作,常用于缩放图片或网页。而 UISwipeGestureRecognizer 用于识别滑动操作,可以添加多个滑动手势识别器来分别处理不同方向的滑动。

// 示例代码:创建并添加轻触手势识别器
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
tapGestureRecognizer.numberOfTapsRequired = 1 // 一次轻触
view.addGestureRecognizer(tapGestureRecognizer)

@objc func handleTap() {
    print("轻触手势被触发")
}

在上述代码中,我们创建了一个单击的手势识别器,并将其添加到视图中。当用户执行轻触操作时,会调用 handleTap 方法。

2.1.2 多点触控与手势识别

多点触控手势识别是iOS中的高级功能,它允许设备识别两个或更多手指的操作。在 UIKit 中, UIRotationGestureRecognizer UIPinchGestureRecognizer 都支持多点触控。多点触控手势的处理相对复杂,但可以通过 location(ofTouch:in:) 方法获取每个触摸点的位置,进而分析手势的运动模式和力度。

对于 UIPinchGestureRecognizer ,开发者可以关注其 scale 属性,这个属性会随着手指捏合的动作发生变化,从而实现图片缩放的功能。

// 示例代码:处理多点触控的缩放手势
let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))

@objc func handlePinch(_ sender: UIPinchGestureRecognizer) {
    if sender.state == .began || sender.state == .changed {
        // 缩放图片或视图
        imageView.transform = CGAffineTransform(scaleX: sender.scale, y: sender.scale)
    } else if sender.state == .ended {
        // 动画结束,可以在这里执行一些动作
        print("缩放手势结束")
    }
}

在该示例中,我们为 UIPinchGestureRecognizer 设置了一个处理器 handlePinch(_:) 。当手势开始或改变时,根据缩放因子 scale 调整图片的缩放级别。手势结束时,可以在控制台输出一条日志,表明手势已经完成。

2.2 手势与UIScrollView的交互实现

2.2.1 滚动事件的手势映射

UIScrollView 是一个非常强大的控件,它允许用户滚动查看内容,而手势识别器可以用来增强或改变 UIScrollView 的默认行为。例如,你可以添加一个长按手势来查看图片的缩略图或执行复制文本的操作。

要将手势映射到 UIScrollView 的滚动事件,可以通过手势识别器的 isEnabled 属性来启用或禁用相应的行为。例如,禁用默认的滚动行为后,可以自定义滚动时的动画效果。

// 禁用UIScrollView的默认滚动行为
scrollView.isScrollEnabled = false

// 自定义滚动事件
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
scrollView.addGestureRecognizer(panGestureRecognizer)

@objc func handlePanGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
    // 根据手势状态来处理
    switch gestureRecognizer.state {
    case .began, .changed, .ended:
        // 更新UIScrollView的contentOffset来实现滚动
        scrollView.contentOffset = gestureRecognizer.translation(in: scrollView)
    default:
        break
    }
}

上面的代码通过 UIPanGestureRecognizer 来控制 UIScrollView 的滚动。当用户开始拖动时,记录下触摸点相对于 UIScrollView 的位置,然后不断更新 contentOffset 来实现滚动效果。

2.2.2 手势冲突的处理策略

当多个手势识别器作用于同一个视图上时,可能会出现手势冲突。在iOS开发中,手势冲突处理非常重要,尤其是当 UIScrollView 和其它手势识别器共存时。 UIGestureRecognizer 类提供了一个 collision 属性,可以用来检测手势识别器之间的冲突,并且我们可以通过设置不同的 delegate 方法来解决冲突。

UIGestureRecognizer require(toFail:) 方法是一个常用的技术,它允许开发者指定某些手势识别器必须失败后,当前手势识别器才能成功。这样可以解决一些典型的手势冲突,例如在用户执行滑动手势时,不触发单击事件。

// 示例代码:设置手势冲突处理
panGestureRecognizer.require(toFail: longPressGestureRecognizer)

// 在手势识别器的代理方法中处理冲突
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return true // 允许手势同时识别
}

在上述代码中,我们让 panGestureRecognizer longPressGestureRecognizer 失败后才能成功,从而处理了一个滑动手势和长按手势之间的潜在冲突。 gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) 方法的返回值 true 允许这两个手势可以同时识别,这在一些特定的交互设计中非常有用。

3. 自动布局保证视图适应不同屏幕

要创建一个能够适应不同屏幕尺寸的应用,开发者需要确保布局的灵活性和可扩展性。自动布局(Auto Layout)是iOS开发中广泛使用的一种机制,旨在简化视图的布局,并确保它们在不同设备和屏幕方向变化时仍然表现一致。本章将深入探讨自动布局的概念、原理以及在UIScrollView中的应用。

3.1 自动布局的概念与原理

3.1.1 自动布局与约束的引入

自动布局的核心是约束(Constraints),它们定义了视图之间以及视图与父视图之间相对位置的关系。开发者通过设置约束来指定视图的大小、位置和方向,而不是通过静态的frame值。这种基于约束的布局方式比传统的frame布局更加灵活和强大。

在引入约束之前,若要适应屏幕大小变化,开发者通常需要手动重写视图的布局代码,这不仅繁琐,而且容易出错。自动布局提供了一种声明式的方式来定义布局,可以极大简化这一过程,让布局代码更加清晰。

3.1.2 约束的种类与设置方法

约束主要有以下几种类型:

  • 等式约束 :确保两个元素之间的尺寸关系等同。例如,一个按钮的宽度与另一个按钮相等。
  • 不等式约束 :保持两个元素之间尺寸的比较关系,如宽度不小于另一个元素。
  • 间距约束 :控制两个元素之间的间距。
  • 相对位置约束 :定义元素相对于另一个元素的位置关系,如左侧对齐。

创建约束通常有以下几种方法:

  • Interface Builder :通过可视化拖拽的方式设置约束,适用于视觉布局优先的场景。
  • 代码中动态设置 :通过编程方式,使用 NSLayoutConstraint 类来定义约束,适用于逻辑复杂或动态布局需求的场景。
  • Storyboard和XIB :在Storyboard或XIB文件中,通过属性面板设置约束,结合Interface Builder使用。

3.2 自动布局在UIScrollView中的应用

3.2.1 创建适应不同屏幕的布局

为了使UIScrollView的内容能够适应不同屏幕尺寸,开发者需要为UIScrollView内的视图设置合适的约束。例如,如果UIScrollView内嵌有一系列的图片视图,可以为每个图片视图设置等宽的约束,并且让它们的水平间距和垂直间距保持一致。当屏幕尺寸变化时,UIScrollView会自动调整图片的显示区域。

3.2.2 约束与UIScrollView滚动行为的关联

在使用自动布局时,开发者需要关注滚动视图的滚动行为与约束的关系。如果UIScrollView的内容尺寸大于其本身尺寸,它将允许用户滚动查看全部内容。开发者需要确保所有子视图的约束在任何情况下都不应互相冲突,否则可能导致滚动行为不正常或应用崩溃。

3.2.3 示例代码与逻辑分析

下面是一个简单的例子,展示如何通过代码给UIScrollView添加约束,保证其内容适应屏幕变化。

// 假设已经有了一个UIScrollView实例名为scrollView
// 这里将为scrollView添加四个子视图,并设置它们的约束

for i in 0..<4 {
    // 创建一个新的UIView作为示例
    let subview = UIView()
    subview.backgroundColor = .lightGray
    scrollView.addSubview(subview)
    // 设置子视图的宽高约束
    let widthConstraint = subview.widthAnchor.constraint(equalToConstant: 100)
    let heightConstraint = subview.heightAnchor.constraint(equalToConstant: 100)
    widthConstraint.isActive = true
    heightConstraint.isActive = true
    // 设置子视图的水平间距约束
    if i > 0 {
        let leadingConstraint = subview.leadingAnchor.constraint(equalTo: views[i-1].trailingAnchor)
        leadingConstraint.isActive = true
    }
    // 设置子视图的垂直间距约束
    subview.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor).isActive = true
}

// 设置UIScrollView的宽度和高度约束,使其能够适应不同屏幕
let widthConstraint = scrollView.widthAnchor.constraint(equalTo: view.widthAnchor)
let heightConstraint = scrollView.heightAnchor.constraint(equalTo: view.heightAnchor)
widthConstraint.isActive = true
heightConstraint.isActive = true

// 使UIScrollView能够滚动
scrollView.contentSize = CGSize(width: 100 * 4, height: 100)

在这个例子中,我们首先创建了四个宽度和高度均为100点的子视图,并将它们添加到UIScrollView中。然后,我们为每个子视图设置了宽高约束,并通过 isActive 属性激活这些约束。对于水平间距,我们通过 leadingAnchor 属性与前一个子视图的 trailingAnchor 关联,形成一个水平链。对于垂直位置,我们将每个子视图的 centerYAnchor 与UIScrollView的 centerYAnchor 对齐。最后,我们为UIScrollView设置了宽度和高度的约束,并设置了 contentSize 属性以允许用户滚动。

通过上述步骤,UIScrollView的子视图能够根据屏幕的大小自动调整,保持布局的整洁和一致性。这展示了自动布局在处理动态界面尺寸变化中的强大功能。

在实际应用中,应确保所有的约束都不会相互冲突,并且在视图控制器的生命周期中,如 viewDidLoad viewDidLayoutSubviews 中,适时地更新约束以响应可能的布局变化。

本章通过深入剖析自动布局的概念与原理以及其在UIScrollView中的具体应用,希望能够帮助开发者设计出更加灵活和适应性强的iOS界面。在后续的章节中,我们将继续探讨图片加载和缓存优化、动画效果的添加等关键技术和优化策略。

4. 图片加载和缓存策略优化性能

随着移动应用的普及,用户对于图片加载的速度和质量提出了更高的要求。一个慢速加载的图片,或者频繁导致应用崩溃的内存溢出,都极大地影响用户体验。因此,优化图片加载和缓存策略对于提升移动应用的性能至关重要。

4.1 图片加载技术的选择与应用

图片加载库在现代移动应用开发中扮演着重要角色。它们通常提供了更为丰富和优化的API来处理图片的加载,使得开发者可以更轻松地实现复杂的图片加载场景。

4.1.1 常用的图片加载库比较

目前市面上流行的图片加载库包括但不限于 SDWebImage , Kingfisher 以及 Alamofire Image 。它们各自的优缺点如下:

  • SDWebImage 是一个较为老牌的图片加载库,支持WebP格式,内存管理较好,但是由于历史原因,API设计较为复杂。
  • Kingfisher 拥有现代的API设计和流畅的链式调用,支持WebP和AVIF格式,易于集成。
  • Alamofire Image 则与网络库 Alamofire 集成紧密,适合于需要网络请求和图片加载紧密耦合的场景。

4.1.2 异步加载与主线程更新UI

图片的异步加载是提高应用性能的重要措施之一。由于图片加载通常是一个耗时的操作,放在主线程执行会阻塞UI的更新,导致应用界面无响应。因此,所有的图片加载操作应该放在后台线程进行,而更新UI的操作则应该回到主线程。

DispatchQueue.global(qos: .background).async {
    let image = downloadImage(from: "imageURL")
    DispatchQueue.main.async {
        imageView.image = image
    }
}

上面的Swift代码展示了如何在后台线程下载图片,并在下载完成后切换回主线程更新图片视图。这里使用了 DispatchQueue 来控制线程切换,通过全局后台队列进行图片下载,下载完成后通过主线程队列来更新UI元素。

4.2 高效的图片缓存策略

图片缓存机制对于减少网络请求、提升应用响应速度和节省流量等多方面都有显著效果。有效的缓存策略可以确保应用在加载图片时,优先从缓存中获取,这样能够大幅提升用户界面的响应性。

4.2.1 缓存机制的设计与实现

常见的缓存策略包括内存缓存和磁盘缓存。内存缓存速度最快,但容量有限;磁盘缓存容量较大,但访问速度比内存缓存慢。一个有效的缓存机制通常会将这两种缓存方式结合起来使用。

if let cachedImage = imageCache.object(forKey: url) as? UIImage {
    imageView.image = cachedImage
} else {
    let imageData = try? Data(contentsOf: URL(string: url)!)
    let image = UIImage(data: imageData!)
    imageCache.setObject(image!, forKey: url)
    imageView.image = image
}

这段代码简单演示了Swift中结合内存和磁盘缓存的加载逻辑。首先尝试从内存缓存中获取图片,如果未命中,再从磁盘中加载。加载成功后,将图片同时存入内存和磁盘。

4.2.2 内存与磁盘缓存的管理

缓存管理的核心在于如何淘汰旧的缓存,为新的缓存腾出空间。常用的策略包括 Least Recently Used (LRU),即最近最少使用原则。当缓存空间不足时,删除最久未被访问的缓存项。

class LRUCache<Key: Hashable, Value> {
    private var cache: OrderedDict<Key, Value> = [:]
    private let capacity: Int

    init(capacity: Int) {
        self.capacity = capacity
    }

    func get(_ key: Key) -> Value? {
        guard let value = cache.removeValue(forKey: key) else { return nil }
        cache[key] = value
        return value
    }

    func set(_ key: Key, _ value: Value) {
        cache[key] = value
        if cache.count > capacity {
            let lastKey = cache.popLast()
            lastKey?.map { cache.removeValue(forKey: $0) }
        }
    }
}

这段Swift代码实现了一个简单的LRU缓存结构。通过 OrderedDictionary 来保证缓存项按照访问顺序排列,当超出容量限制时,自动淘汰最久未访问的项。

此外,使用第三方库如 Kingfisher 的缓存机制,可以简化实现和提高缓存管理的效率。例如, Kingfisher 在下载图片时会自动缓存到磁盘,开发者可以自定义内存缓存策略,达到优化应用性能的目的。

图片加载和缓存策略的优化是移动应用性能提升的重要方面。通过合理的图片加载库选择,以及高效缓存机制的设计和实现,可以显著提升用户体验和应用性能。

5. 利用UIView和CATransition添加动画效果

5.1 UIView动画的基础知识

在iOS应用开发中,动画是提升用户体验的重要手段。通过添加动画,静态的界面元素可以更加生动和吸引用户的注意力。UIView提供了简单的动画API,使得开发者可以轻松地为界面元素添加各种动画效果。

5.1.1 动画的基本类型与实现

UIView的动画可以大致分为两类:隐式动画和显式动画。隐式动画是基于属性改变时自动触发的动画,而显式动画则需要开发者手动创建和控制。

要实现一个简单的显式动画,可以使用 UIView animateWithDuration:animations: 方法,如下示例代码展示了如何为一个视图添加一个旋转动画:

UIView.animate(withDuration: 1.0, animations: {
    myView.transform = CGAffineTransform(rotationAngle: CGFloat.pi * 2)
}, completion: nil)

这段代码将 myView 视图旋转360度,动画持续时间为1秒。在这个过程中, animations 块内定义了属性的变化,而 completion 块则用于动画执行完毕后的回调。

5.1.2 动画时长与缓动函数的应用

动画的时长直接影响用户的视觉感受。开发者可以自定义动画时长,甚至可以使用不同的缓动函数来控制动画的变化速度。缓动函数决定了动画在运动过程中的速度变化,常见的有线性缓动、加速缓动、减速缓动、弹跳缓动等。

示例代码演示了如何添加一个弹簧效果的动画:

let springAnimation = UIViewPropertyAnimator(duration: 0.5, curve: .spring)
springAnimation.addAnimations {
    myView.center = CGPoint(x: 100, y: 100)
}
springAnimation.startAnimation()

这段代码定义了一个弹簧动画,将 myView 视图的位置移动到(100, 100)点,并且使用了弹簧效果。

5.2 CATransition动画的高级应用

CATransition Core Animation 框架中用于添加过渡动画的类。它允许开发者为视图层添加复杂的动画效果,如推入、揭开、溶解等。

5.2.1 CATransition的类型和属性设置

CATransition 提供了多种动画类型,可以通过设置 type subtype 属性来实现不同的动画效果。

示例代码展示了如何给一个视图添加一个“溶解”动画效果:

let transition = CATransition()
transition.type = CATransitionType.dissolve
transition.duration = 1.0
transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
myView.layer.add(transition, forKey: "dissolve")

在这个例子中,我们创建了一个 CATransition 对象,设置了动画类型为 dissolve ,时长为1秒,并使用了 easeInEaseOut 缓动函数。然后,我们将这个动画添加到 myView 层的动画集合中。

5.2.2 动画转换效果的定制与优化

为了实现更加个性化和优化的动画效果,开发者可以自定义 CAMediaTimingFunction 或者为动画添加多个阶段和复杂的动画属性。

示例代码展示了如何创建一个自定义的缓动函数,并将其应用到 CATransition 中:

let timingFunction = CAMediaTimingFunction(controlPoints: 0.5, 0.0, 0.9, 1.0)
let transition = CATransition()
transition.type = CATransitionType.push
transition.subtype = CATransitionSubtype.fromRight
transition.duration = 1.5
transition.timingFunction = timingFunction
myView.layer.add(transition, forKey: "customPush")

在这段代码中,我们自定义了一个缓动函数,并将其应用到一个推入类型的动画中,这种动画看起来会有一个慢入快出的效果。

通过上述章节的介绍,我们可以看到 UIView CATransition 在iOS开发中添加动画的强大功能。合理地利用这些API,可以显著提升应用的用户体验和界面的互动性。

6. 性能优化:图片预加载、内存管理

6.1 图片预加载的策略和实现

图片预加载是一种常见的优化策略,用于提前加载即将展示的图片资源,以避免在用户滚动查看图片时出现延迟加载的问题,从而提升用户体验。

6.1.1 预加载的时机与方法

在iOS开发中,预加载可以通过多种方式实现。一种是静态预加载,即在程序启动或者进入相关视图控制器时,预先加载所有可能被查看的图片。另一种是动态预加载,它根据用户的滚动方向或行为来预加载邻近的图片。

静态预加载示例代码

// 假设有一个图片名称数组,包含将要加载的图片资源
let imageNames = ["image1.jpg", "image2.jpg", "image3.jpg"]
var imageCache = [String:UIImage]()

func preloadImages(_ names: [String]) {
    for imageName in names {
        if let image = UIImage(named: imageName) {
            imageCache[imageName] = image
        }
    }
}

preloadImages(imageNames)

6.1.2 预加载对用户体验的影响

预加载能够显著改善滚动时的性能,但使用不当也会带来负面影响。大量预加载可能会增加应用的启动时间和内存占用。因此,合理的预加载策略要根据实际的用户行为和应用环境来定。可以通过分析用户行为数据,决定哪些图片资源需要预加载,哪些可以动态加载。

6.2 内存管理的最佳实践

在iOS开发中,内存管理是保证应用性能和稳定性的重要环节。即使在现代iOS设备拥有大量内存的环境下,仍然需要开发者采取措施,合理分配和优化内存使用。

6.2.1 内存泄漏的识别与避免

内存泄漏通常是由于对象被错误地持有,导致它们在不应该的时候依然被占用内存。在Xcode中,可以利用Instruments工具中的Allocations和Leaks来检测内存泄漏。

避免内存泄漏的代码示例

// 避免强引用循环
class A {
    var b: B?
}

class B {
    weak var a: A? // 使用weak防止强引用循环
}

6.2.2 应用内存压力测试与优化

为了确保应用在不同环境下都能保持良好的性能,进行内存压力测试是非常必要的。Xcode提供了内存压力测试工具,可以模拟不同的内存压力情况来测试应用的行为。

内存压力测试和优化步骤

  1. 在Xcode中打开Product -> Scheme -> Edit Scheme。
  2. 选择Run,然后切换到Diagnostics标签页。
  3. 勾选Memory Assertions来使应用在内存压力较大时进行断言。
  4. 启动应用后,Xcode控制台将显示内存警告信息和断言失败的地方。

通过这样的测试,开发者能够发现应用在高内存压力下的问题,并针对性地进行优化,比如释放未使用的资源、优化图片尺寸、使用更小的数据类型等。

通过合理的预加载策略以及有效的内存管理,可以极大提升应用的性能和用户体验。性能优化是一个持续的过程,需要在应用的整个生命周期内持续监控和调整。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:在iOS开发中,实现图片浏览的左右上下滑动切换功能是常见的交互设计,适用于图片浏览应用和游戏等。通过使用UIScrollView来设定可滚动区域,并通过手势识别处理用户滑动动作,开发者可以为用户界面添加平滑的图片切换动画效果。文章将介绍UIScrollView的使用、手势识别器的集成、自动布局的应用、图片加载与缓存策略、动画效果的实现以及性能优化技巧,帮助开发者掌握如何构建流畅的图片浏览体验。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值