SwiftUI之深入解析高级布局的实战教程

news2024/11/16 17:54:50

一、自定义动画

  • 首先实现一个圆形布局的视图容器 WheelLayout:

在这里插入图片描述

struct ContentView: View {
    let colors: [Color] = [.yellow, .orange, .red, .pink, .purple, .blue, .cyan, .green]
    
    var body: some View {
        WheelLayout(radius: 130.0, rotation: .zero) {
            ForEach(0..<8) { idx in
                RoundedRectangle(cornerRadius: 8)
                    .fill(colors[idx%colors.count].opacity(0.7))
                    .frame(width: 70, height: 70)
                    .overlay { Text("\(idx+1)") }
            }
        }
    }
}

struct WheelLayout: Layout {
    var radius: CGFloat
    var rotation: Angle
    
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        
        let maxSize = subviews.map { $0.sizeThatFits(proposal) }.reduce(CGSize.zero) {
            return CGSize(width: max($0.width, $1.width), height: max($0.height, $1.height))
        }
        
        return CGSize(width: (maxSize.width / 2 + radius) * 2,
                      height: (maxSize.height / 2 + radius) * 2)
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        let angleStep = (Angle.degrees(360).radians / Double(subviews.count))

        for (index, subview) in subviews.enumerated() {
            let angle = angleStep * CGFloat(index) + rotation.radians
            // Find a vector with an appropriate size and rotation.
            var point = CGPoint(x: 0, y: -radius).applying(CGAffineTransform(rotationAngle: angle))
            // Shift the vector to the middle of the region.
            point.x += bounds.midX
            point.y += bounds.midY
            
            // Place the subview.
            subview.place(at: point, anchor: .center, proposal: .unspecified)
        }
    }
}
  • 当布局发生改变时,SwfitUI 提供了内置动画支持,因此如果将轮子的旋转值更改为 90 度,我们将会看见它是如何逐渐的移动到新的位置上:

在这里插入图片描述

WheelLayout(radius: radius, rotation: angle) {
// ...
}

Button("Rotate") {
    withAnimation(.easeInOut(duration: 2.0)) {
        angle = (angle == .zero ? .degrees(90) : .zero)
    }
}
  • 渲染动画的逻辑就是如此,那么我们在探索一下深层次到底发生了什么?当改变角度时,SwiftUI 会计算好每个视图最初和最终的位置,然后在动画期间内修改它们的位置,从 A 点到 B 点成一条直线,起初它似乎没有这样做,但是检查下面这个动画,集中注意观察单个视图,看看它们是如何都跟随直虚线移动的?

在这里插入图片描述

  • 如果动画的角度是从 0 到 360 会发生什么吗?其实,什么都不会发生,开始的位置和结束的位置是一样的,因此就 SwiftUI 而言,没有动画。由于我们将视图围绕一个圆圈放置,如果视图沿着那个假想的圆圈移动不是更有意义吗?好吧,事实证明,这样做非常容易,这个布局协议采用 Animatable 协议,如果你还不了解这个协议,可以参考我之前的博客:SwiftUI之深入解析布局协议的功能与布局的实现教程。
  • 简单的说,通过添加 animatableData 属性到布局,我们要求 SwiftUI 动画的每一帧重新计算布局。但是,在每个布局传递中,角度都会收到一个内插值,现在 SwiftUI 不会为我们插入位置。相反,它会插入角度值,我们的布局代码将会完成剩下的工作。

在这里插入图片描述

struct Wheel: Layout {
    // ...
    var animatableData: CGFloat {
        get { rotation.radians }
        set { rotation = .radians(newValue) }
    }
    // ...
}
  • 添加一个 animatableData 属性足够让视图正确的跟随圆圈。但是,既然已经到了这步,为什么不让半径也可以动画呢?
var animatableData: AnimatablePair<CGFloat, CGFloat> {
    get { AnimatablePair(rotation.radians, radius) }
    set {
        rotation = Angle.radians(newValue.first)
        radius = newValue.second
    }
}

二、双向自定义值

  • 在上文中,了解到如何使用 LayoutValues 将信息附加到视图,以便它们的代理可以在 placeSubviews 和 sizeThatFits 方法中暴露这些信息,我们的想法是信息从视图流向布局,一会儿将看见这一点是如何被逆转。
  • 假设想要视图旋转起来,让它们指向中心:

在这里插入图片描述

  • 布局协议只能决定视图位置和它们的建议尺寸,但是不能应用样式、旋转或者其他的效果。如果想要这些效果,那么布局应该有一种传达回视图的方式,这时候布局值就变得重要起来,到目前为止,我们已经使用它们传递信息给布局,但只要加上一点创意,就可以反向使用它们。
  • LayoutValues 并不局限于传递 CGFloats ,可以将它用于任何事情,包括 Binding,在这里将使用 Binding:
struct Rotation: LayoutValueKey {
    static let defaultValue: Binding<Angle>? = nil
}
  • 之所以称它为双向自定义值,因为信息是可以双向流动的,但是,这不是 SwiftUI 的官方术语,只是为了更清晰的解释这个想法的术语。在布局的 placeSubview 方法中,设置每个子视图的角度:
struct WheelLayout: Layout {
    // ...
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        let angleStep = (Angle.degrees(360).radians / Double(subviews.count))

        for (index, subview) in subviews.enumerated() {
            let angle = angleStep * CGFloat(index) + rotation.radians
            // ...
            DispatchQueue.main.async {
                subview[Rotation.self]?.wrappedValue = .radians(angle)
            }
        }
    }
}
  • 回到视图,可以读取值,并用它来旋转视图:
struct ContentView: View {
    // ...
    @State var rotations: [Angle] = Array<Angle>(repeating: .zero, count: 16)
    
    var body: some View {    
        WheelLayout(radius: radius, rotation: angle) {
            ForEach(0..<16) { idx in
                RoundedRectangle(cornerRadius: 8)
                    .fill(colors[idx%colors.count].opacity(0.7))
                    .frame(width: 70, height: 70)
                    .overlay { Text("\(idx+1)") }
                    .rotationEffect(rotations[idx])
                    .layoutValue(key: Rotation.self, value: $rotations[idx])
            }
        }
        // ...
}
  • 这段代码将确保所有的视图都指向圆心,但是可以更优雅一点。这里提供的解决方案需要设置一个旋转数组,将它们作为布局值然后使用这些值旋转视图,如果可以向布局用户隐藏这种复杂性那不是很好吗?这里就是重写之后的。
  • 首先创建一个封装视图 WheelComponent:
struct WheelComponent<V: View>: View {
    @ViewBuilder let content: () -> V
    @State private var rotation: Angle = .zero   
    var body: some View {
        content()
            .rotationEffect(rotation)
            .layoutValue(key: Rotation.self, value: $rotation)
    }
}
  • 然后摆脱旋转数组,并将每个视图包装在 WheelComponent 视图中:
WheelLayout(radius: radius, rotation: angle) {
    ForEach(0..<16) { idx in
        WheelComponent {
            RoundedRectangle(cornerRadius: 8)
                .fill(colors[idx%colors.count].opacity(0.7))
                .frame(width: 70, height: 70)
                .overlay { Text("\(idx+1)") }
        }

    }
}
  • 用户使用容器只需要记住将视图封装在 WheelComponent 里面,它们不需要担心布局值、绑定、角度等。当然,不在封装里的视图不会受到任何影响,视图不会旋转指向中心。
  • 还可以添加一个改进,那就是视图旋转的动画,仔细观察并比较下面三个轮子:一个不旋转,另外两个旋转指向中心,但是一个不使用动画而另一个使用。

三、避免布局循环和崩溃

  • 众所周知,在布局期间不能更新视图状态,这会导致不可预测的结果,很可能会使 CPU 达到峰值。在此之前我们看到过这种情况,即闭包在布局期间运行时,也许当时不是太明显。但是现在,这是毫无疑问的,sizeThatFits 和 placeSubviews 是布局过程中的一部分。因此当使用上一部分中描述的"欺骗"的技巧,必须使用 DispatchQueue 用队列更新。就像上面的例子一样:
DispatchQueue.main.async {
    subview[Rotation.self]?.wrappedValue = .radians(angle)
}
  • 使用双向自定义值还有另一个潜在的问题,那就是视图必须在不影响别的布局的前提下使用该值,否则的话将会陷入布局循环。例如,如果用 placeSubviews 设置去更改视图颜色,那就是安全的。在案例中,可能看起来旋转会影响布局,但其实不是这样的,当旋转视图,它的周围从来没产生影响,边界仍然保持不变。如果设置了偏移,或者其他的变换矩阵,也会发生同样的事情。可以监测 CPU 来发现布局中其他潜在的问题,如果 CPU 开始飙升,或许可以在 placeSubviews 中添加一条打印语句查看它是否无休止的调用。注意动画也会使 CPU 增长,如果想测试容器是否循环,不要在动画时查看 CPU 。
  • 注意这不是新问题,过去在使用 GeometryReader 获取视图尺寸并传递值到父视图的时候遇到过这个问题,然后父视图使用该信息去改变视图,即使用 GeometryReader 去再一次改变,然后就陷入了布局循环,这是个老问题,在 SwiftUI 刚发布的时候就写过此类问题。
  • 再说一下潜在的崩溃,这与双向自定义值无关,这是在写任何布局都必须要考虑的,提到 SwiftUI 可能会多次调用 sizeThatFits 去测试视图的灵活性。在这些调用中,返回的值应该是合理的。例如,下面的代码会崩溃:
struct ContentView: View {
    var body: some View {
        CrashLayout {
            Text("Hello, World!")
        }
    }
}

struct CrashLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        if proposal.width == 0 {
            return CGSize(width: CGFloat.infinity, height: CGFloat.infinity)
        } else if proposal.width == .infinity {
            return CGSize(width: 0, height: 0)
        }
        
        return CGSize(width: 0, height: 0)
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) 
    {
    }
}
  • 这里 sizeThatFits 返回 .infinity 作为最小尺寸,.zero 作为最大尺寸,这是不合理的,最小尺寸不可能大于最大尺寸。

四、递归布局

  • 把之前的 WheelLayout 视图转变为 RecursiveWheel,新布局在圆圈里放置 12 个视图,里面的 1 2个视图将会按比例缩小到内圈中,直到它们不会再有别的视图,视图的缩放和旋转要再一次使用双向自定义值实现。在这个例子中在容器中一共有 44 个视图,因此新容器将会分别以 12、12、12 和 8 为一圈。
  • 注意本案例中如何使用缓存与子视图通信,这是可以实现的,因为缓存是一个 inout 参数,可以在 placeSubviews 中更新。placeSubviews 方法遍历并放置 12 个子视图:
for (index, subview) in subviews[0..<12].enumerated() {
  // ...
}
  • 然后递归调用 placeSubviews 但仅限于剩下的视图,如此直到没有别的视图:
placeSubviews(in: bounds,
              proposal: proposal,
              subviews: subviews[12..<subviews.count],
              cache: &cache)

五、布局组合

  • 在上一个例子中,使用了相同的布局递归。这里也可以组合一些不同布局到容器中,把前三个视图水平的放置在视图顶部,后三个水平的放置在底部,剩下的视图将会在中间,垂直排列:

在这里插入图片描述

  • 不需要编写垂直或者水平的间距逻辑代码,因为 SwiftUI 已经有这样的布局:HStackLayout 和 VStackLayout,只是有点小问题,很轻易就可以修复。由于某些原因,系统布局私下实现了 sizeThatFits 和 placeSubviews,这意味着无法调用它们。但是,类型消除布局确实暴露它的所有方法,因此不要这样做:
HStackLayout(spacing: 0).sizeThatFits(...) // not possible
  • 可以使用如下的方法:
AnyLayout(HStackLayout(spacing: 0)).sizeThatFits(...) // it is possible!
  • 此外,在与其他视图布局工作的时候,就相当于 SwiftUI 的角色,子布局的任何缓存创建和更新都属于我们的责任,幸运的是,这都很容易处理,只需要添加子布局缓存到自己的缓存里:
struct ComposedLayout: Layout {
    private let hStack = AnyLayout(HStackLayout(spacing: 0))
    private let vStack = AnyLayout(VStackLayout(spacing: 0))

    struct Caches {
        var topCache: AnyLayout.Cache
        var centerCache: AnyLayout.Cache
        var bottomCache: AnyLayout.Cache
    }

    func makeCache(subviews: Subviews) -> Caches {
        Caches(topCache: hStack.makeCache(subviews: topViews(subviews: subviews)),
               centerCache: vStack.makeCache(subviews: centerViews(subviews: subviews)),
               bottomCache: hStack.makeCache(subviews: bottomViews(subviews: subviews)))
    }

    // ...
}

六、插入布局

  • 另一个组合案例:插入两个布局下一个例子将会创建一个以轮子,或者波浪形式显示视图的布局。当然它还提供了一个从 0.0 到 1.0 的 pct 参数,当 pct == 0.0,视图将会展示轮子,当 pct == 1.0,视图将会展示 sin 波动,中间的数值将会穿插在两者的位置之中。
  • 在创建组合布局之前,先来看一下 WaveLayout,这个布局有好几个参数来改变正弦波动的幅度,频率和角度。InterpolatedLayout 将会计算两个布局(波动和轮子)的尺寸和位置然后它将插入这些值以进行最终定位。在 placeSubviews 方法中,如果子视图被多次定位,最后一次调用 place() 才会产生影响。
  • 使用以下公式计算插值:
(wheelValue * pct) + (waveValue * (1-pct))
  • 现在需要一种方法让 WaveLayout 和 WheelLayout 将每一个视图位置和旋转返回给 InterpolatedLayout,可以通过缓存做到了这一点,再一次看到了缓存不是性能提升的唯一用途。我们也需要 WaveLayout 和 WheelLayout 去检测它们是否被 InterpolatedLayout 使用,以便它们可以相应的更新缓存。这些视图可以轻易的检测到这种情况,这要归功于独立的缓存值,如果缓存是由 InterpolatedLayout 创建的,则该值仅为 false。

七、使用绑定参数

  • 今年 SwfitUI Lounges 出现了一个有趣的问题,询问是否可能使用新的布局协议去创建一个层次树,用线连接。挑战的不是视图树结构,而是如何画连接线。

在这里插入图片描述

  • 还有其它方法可以实现它,例如,使用 Canvas,但是这里都是关于布局协议的,来看看可以如何解决连接线的问题。我们都知道,这根线不可能被布局绘制出来,那需要的是一种让布局告诉视图如何绘制线条的方法。初步想法可以使用布局值,这正是在上一个例子中做的事情,双向自定义值。但是,仔细思考之后,还有一种更简单的方式。
  • 相比于使用布局值去分别通知树的每个节点的最终位置,使用布局代码创建整个路径来的更简单一点,然后只需要将路径返回给负责展示的视图,通过添加绑定布局参数很容易完成:
struct TreeLayout {
    @Binding var linesPath: Path
    // ...
}
  • 在完成放置视图以后,知道了位置并使用它们的坐标去创建路径。再次注意,必须非常小心的避免布局循环,更新路径会产生一个循环,即使该路径被绘制为不影响布局的背景视图也是如此,所以为了避免这种循环,要确保路径发生改变,然后才更新绑定,这样就可以成功的打破循环。
let newPath = ...

if newPath.description != linesPath.description {
    DispatchQueue.main.async {
        linesPath = newPath
    }
}
  • 这个挑战的另一个有趣的部分是告诉布局这些视图如何分层连接,在本例中,创建了两个 UUID 布局值,一个标识视图,另一个作为父视图的 ID:
struct ContentView: View {
    @State var path: Path = Path()
    
    var body: some View {
        let dash = StrokeStyle(lineWidth: 2, dash: [3, 3], dashPhase: 0)

        TreeLayout(linesPath: $path) {
            ForEach(tree.flattenNodes) { node in
                Text(node.name)
                    .padding(20)
                    .background {
                        RoundedRectangle(cornerRadius: 15)
                            .fill(node.color.gradient)
                            .shadow(radius: 3.0)
                    }
                    .node(node.id, parentId: node.parentId)
            }
        }
        .background {
            // Connecting lines
            path.stroke(.gray, style: dash)
        }
    }
}

extension View {
    func node(_ id: UUID, parentId: UUID?) -> some View {
        self
            .layoutValue(key: NodeId.self, value: id)
            .layoutValue(key: ParentNodeId.self, value: parentId)
    }
}
  • 当使用这些代码的时候需要注意一些事项,这里应该只有一个父节点是 nil 的节点(根结点),因此应该小心的避免循环引用(例如:两个节点互为父节点)。同时也要注意,这里有一个好的选择,即放置到具有垂直和水平的滚动 ScrollView 中。注意这是基本实现,仅用于说明如何实现,还有许多潜在的优化,但制作树布局所需的关键元素都在这里。

八、调试

  • 回到当 SwiftUI 刚发布的时候,尽力搞清楚布局是如何工作的,有这么一个工具直到现在,用来添加围绕视图的边框观察视图边缘,使用边框依然是很好的调试工具,但可以添加一个新的工具。创建一个修饰器,它在尝试理解为什么视图不像认为的那样工作的时候非常有用,修饰器在这儿:
func showSizes(_ proposals: [MeasureLayout.SizeRequest] = [.minimum, .ideal, .maximum]) -> some View
  • 可以在任何视图使用它,一个覆盖层将会浮动在视图的头部角落,显示一组给定的建议尺寸,如果未制定建议,最小、理想和最大尺寸都将被覆盖。
MyView()
    .showSizes()
  • 部分使用示例如下:
showSizes() // minimum, ideal and maximum
showSizes([.current, .ideal]) // the current size of the view and the ideal size
showSizes([.minimum, .maximum]) // the minimum and maximum
showSizes([.proposal(size: ProposedViewSize(width: 30, height: .infinity))]) // a specific proposal
  • 更多例子:

在这里插入图片描述

ScrollView {
    Text("Hello world!")
}
.showSizes([.current, .maximum])

Rectangle()
    .fill(.yellow)
    .showSizes()

Text("Hello world")
    .showSizes()

Image("clouds")
    .showSizes()

Image("clouds")
    .resizable()
    .aspectRatio(contentMode: .fit)
    .showSizes([.minimum, .ideal, .maximum, .current])
  • 突然间一切都变得有意义了,例如检查一下使用和不使用 resizable() 的图像尺寸,终于能看到数字是不是有一种奇怪的满足感?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1385556.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

强化学习应用(三):基于Q-learning的物流配送路径规划研究(提供Python代码)

一、Q-learning算法简介 Q-learning是一种强化学习算法&#xff0c;用于解决基于马尔可夫决策过程&#xff08;MDP&#xff09;的问题。它通过学习一个值函数来指导智能体在环境中做出决策&#xff0c;以最大化累积奖励。 Q-learning算法的核心思想是使用一个Q值函数来估计每…

纳米量级晶圆表面微观检测技术

持续更新 背景&#xff1a;晶圆表面形状偏差分为&#xff1a;宏观几何误差&#xff0c;中间几何误差&#xff0c;微观几何误差&#xff0c;跟别用表面形状误差&#xff0c;表面波纹度&#xff0c;表面粗度来描述。 主要技术&#xff1a;微分剪切干涩显微技术&#xff0c;五步…

Dubbo分层设计之Transport层

前言 Dubbo 框架采用分层设计&#xff0c;最底下的 Serialize 层负责把对象序列化为字节序列&#xff0c;再经过 Transport 层网络传输到对端。一次 RPC 调用&#xff0c;在 Dubbo 看来其实就是一段请求报文和一段响应报文的传输过程。 理解Transport Transport 层即网络传输…

计算机毕业设计----SSH在线水果商城平台含管理系统

项目介绍 本项目分为前后台&#xff0c;分为普通用户与管理员两个角色&#xff0c;前台为普通用户登录&#xff0c;后台为管理员登录&#xff1b; 管理员角色包含以下功能&#xff1a; 管理员登录,修改密码,类别管理,水果管理,订单管理,网站论坛管理,网站公告管理等功能。 …

抖音小店2024年创业新趋势,新手找项目,不要再错过这次的机会了

大家好&#xff0c;我是电商花花。 现在的抖音小店完全是电商创业中的一个优秀代名词和最轻便的创业项目&#xff0c;更是以独特的直播达人带货的优势将店铺激发出来。 今天给大家介绍下抖音小店的运作方式&#xff0c;并分析互联网创业的机遇&#xff0c;并提供相关的再做点…

Unity中URP下 SimpleLit框架

文章目录 前言一、整体框架1、该Shader是用于低端设备的2、包含一个Properties3、只有一个SubShader4、如果SubShader错误&#xff0c;返回洋葱紫5、调用自定义ShaderGUI面板 二、SubShader中1、Tags2、Pass 三、我们看一下ForwardLit的Pass1、混合模式、深度写入、面皮剔除、透…

ZooKeeper 简介

1、概念介绍 ZooKeeper 是一个开放源码的分布式应用程序协调服务&#xff0c;为分布式应用提供一致性服务的软件&#xff0c;由雅虎创建&#xff0c;是 Google Chubby 的开源实现&#xff0c;是 Apache 的子项目&#xff0c;之前是 Hadoop 项目的一部分&#xff0c;使用 Java …

提高执行力,关键在于管理者做到这四个字

执行力&#xff0c;对于个人而言&#xff0c;它就是办事的效能&#xff1b;而对于领导来说&#xff0c;它是管理的能力。 老板命令员工去买复印纸&#xff0c;员工第一次买回了一沓复印纸&#xff0c;第二次买了三摞复印纸&#xff0c;却仍然没有得到老板的满意。员工之所以跑…

Halcon滤波器 laplace 算子

Halcon滤波器 laplace 算子 使用laplace 算子对图像进行二次求导&#xff0c;会在边缘产生零点&#xff0c;因此该算子常常与zero_crossing算子配合使用。求出这些零点&#xff0c;也就得到了图像的边缘。同时&#xff0c;由于laplace算子对孤立像素的响应要比对边缘或线的响应…

element upload 自定义上传 报错Cannot set properties of null (setting ‘status‘)

element upload 自定义上传 报错Cannot set properties of null (setting ‘status’) 问题展示 原因分析 自定义上传方式 fileList 显示一切正常&#xff0c;状态也是成功 文件url通过URL.createObjectURL(file.raw) 进行添加 以下为配置代码 <el-uploadclass"uplo…

【K12】Python写串联电阻问题的求解思路解析

问题源代码 方法&#xff1a;calculate_circuit_parameter 构造题目&#xff1a; 模板&#xff1a; 已知电阻R1为 10Ω&#xff0c;电阻R2为 5Ω&#xff0c;电压表示数为2.5V&#xff0c;求电源电压U&#xff1f; 给合上面题目&#xff0c;利用Python程序&#xff0c;可以任…

【ScienceAI Weekly】DeepMind拆分的AI药企达成30亿美元新协议;网传字节跳动在美招聘生物/化学/物理人才

AI for Science 的新成果、新动态、新视角—— 由 DeepMind 拆分的 AI 药企首次达成制药合作&#xff0c;价值 30 亿美元微软协助科研人员发现 3,200 万种新电池材料网传 TikTok 在美国各地招募计算生物学、量子化学、分子动力学和物理方面的人才科大讯飞拟分拆医疗业务在港交…

遥感卫星影像现拍,哪里想看拍哪里!

我们为大家分享了查看实时卫星影像的方法。 虽然这个网站的卫星影像10分钟一更新&#xff0c;让世界尽收眼底&#xff0c;但分辨率却非常有限。 如果项目中需要更高清的卫星影像&#xff0c;且对时效性又有较高的要求&#xff0c;那么可以考虑用卫星专门拍摄。 光学遥感卫星…

为什么有人说PMP是水证,它的含金量到底怎么样?

在我国大陆&#xff0c;有好多证书被商业化得太重了&#xff0c;甚至演变成了个人或一些公司摇钱的工具。所以有些证书受人吹捧它崛起的快&#xff0c;但是活不长&#xff0c;甚至“夭折”&#xff0c;比如以前微软系列的证书&#xff1b; 而PMP认证从国外引进大陆这么多年了&…

【昕宝爸爸小模块】守护线程、普通线程、两者之间的区别

➡️博客首页 https://blog.csdn.net/Java_Yangxiaoyuan 欢迎优秀的你&#x1f44d;点赞、&#x1f5c2;️收藏、加❤️关注哦。 本文章CSDN首发&#xff0c;欢迎转载&#xff0c;要注明出处哦&#xff01; 先感谢优秀的你能认真的看完本文&…

打造完美跨境商城源码,助你轻松进军国际市场

随着全球化的深入&#xff0c;跨境电商已成为各国企业拓展国际市场的重要途径之一。根据最新数据显示&#xff0c;跨境电商市场规模逐年扩大&#xff0c;预计未来几年将保持较高增长率。因此&#xff0c;拥有一套完善的跨境商城源码成为企业进军国际市场的关键。 跨境商城源码…

Java--ListUtil工具类,实现将一个大列表,拆分成指定长度的子列表

文章目录 前言实现代码执行结果 前言 在项目中有时会出现列表很大&#xff0c;无法一次性批量操作&#xff0c;我们需要将列表分成指定大小的几个子列表&#xff0c;一份一份进行操作&#xff0c;本文提供这样的工具类实现这个需求。 实现代码 以下为ListUtil工具类代码实现…

【数据结构和算法】删除链表的中间节点

其他系列文章导航 Java基础合集数据结构与算法合集 设计模式合集 多线程合集 分布式合集 ES合集 文章目录 其他系列文章导航 文章目录 前言 一、题目描述 二、题解 三、代码 四、复杂度分析 前言 这是力扣的1657题&#xff0c;难度为中等&#xff0c;解题方案有很多种&…

Java 树形结构数据生成导出excel文件V2

** >> 相对于V1版本&#xff0c;优化了代码逻辑&#xff0c;合理使用递归计算树数据的坐标 << ** 1、效果 2、使用方法 import com.alibaba.fastjson.JSONArray; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.Workboo…

[HTML]Web前端开发技术12(HTML5、CSS3、JavaScript )——喵喵画网页

希望你开心&#xff0c;希望你健康&#xff0c;希望你幸福&#xff0c;希望你点赞&#xff01; 最后的最后&#xff0c;关注喵&#xff0c;关注喵&#xff0c;关注喵&#xff0c;佬佬会看到更多有趣的博客哦&#xff01;&#xff01;&#xff01; 喵喵喵&#xff0c;你对我真的…