五、iWriter 分割视图的实现

Hi, 大家好,我是姜友华。上一节我们实现了标签视图,这一节我们来实现分割视图。

在UIKit里是有类似的SplitView的,但在SwiftUI里我没有找到,所以需要自己来实现。你也可以通过封装`NSSplitViewController 和NSSplitView来实现,这个也留到以后去讲。

这里对分割视图的设计比较简单:首先是接收一组视图,按单一方向排列,中间有分割符隔开即可;其次是实现通过拖动割符隔改变视图的大小。

一、排列的设计。

排列的处理比较简单,接收数组按水平或垂直排列,各区域的大小按百分比来处理即可。

先看代码和显示效果。

    @State var layouts: [AnyView] = []   // 需要排列的对象。
    @State var ratios: [CGFloat] = []
    @State var isHorizontal  = true  // 是否为水平排列。
    
    var body: some View {
        ZStack{
        GeometryReader{ geometry in
            if isHorizontal {
                HStack(spacing: 0) {
                    ForEach(Array(layouts.enumerated()), id: \.offset) { index, layout in
                        layout
                            .frame( width: frameWidth(index, geometry.size.width))
                        if index < layouts.count - 1 {
                            dividerView(index, geometry.size)
                        }
                    }
                }
            } else {
                VStack(spacing: 0) {
                    ForEach(Array(layouts.enumerated()), id: \.offset) { index, layout in
                        layout
                            .frame( height: frameHeight(index, geometry.size.width))
                        if index < layouts.count - 1 {
                            dividerView(index, geometry.size)
                        }
                    }
                }
            }
        }
        }
    }
    
    func frameWidth(_ index: Int, _ width: CGFloat) -> CGFloat {
        return (width - 10 ) * ratios[index]
    }
    
    func frameHeight(_ index: Int, _ height: CGFloat) -> CGFloat {
        return (height - 1) * ratios[index]
    }
    
    // 分割线。
    func dividerView(_ index: Int, _ size: CGSize) -> some View {
        ......
    }
    
    // 拖动分割线。
    func splitDrag(size: CGSize, current: Int) -> some Gesture {
       ......
            }
    }
}

struct SplitView_Previews: PreviewProvider {
    static var previews: some View {
        SplitView(layouts: [
            AnyView(Text("Window 1").background(.red)),
            AnyView(Text("Window 2").background(.red))
        ], ratios: [0.5, 0.5])
    }
}

效果

这个比较好理解,按水平或垂直排列视图。水平的按宽设置占比,垂直的按高设置占比。这里又用到了GeometryReader,用来获取布局的尺寸。

需要说明的是,视图宽高的计算return (width - 10 ) * ratios[index],都是减去一个常量再按占比算,这个常量是分割线的宽。

二、实现分割线的拖动。

分割线的拖动只需要按水平或垂直方向去处理。拖动时动态修改分割线的位置和视图的大小。
直接看代码。

struct SplitView: View {
......
    // 分割线。
    func dividerView(_ index: Int, _ size: CGSize) -> some View {
        Divider()
            .frame(width:1)
            .onHover { inside in
                // 鼠标风络。
                if inside {
                    if isHorizontal {
                        NSCursor.resizeLeftRight.push()
                    } else {
                        NSCursor.resizeUpDown.push()
                    }
                } else {
                    NSCursor.pop()
                }
            }
            .gesture(
                splitDrag(size: size ,current: index)
            )
    }
    
    // 拖动分割线。
    func splitDrag(size: CGSize, current: Int) -> some Gesture {
        DragGesture()
            .onChanged { value in
                // 分割线所有的百分比。
                let value = isHorizontal ? value.location.x : value.location.y
                let toStart = value < 0
                let offset = abs(value) / (isHorizontal ? size.width : size.height)
                let minSize = splitMinSize / (isHorizontal ? size.width : size.height)
                var result = offset
                var array: [CGFloat] = []
                ratios.forEach{ ratio in
                    array.append(ratio)
                }
                
                // 向左或上。
                if toStart {
                    for i in stride(from: current, through: 0, by: -1) {
                        if result < 0.001 {
                            break
                        }
                        // 足够。
                        if array[i] > result + minSize  {
                            array[i] -= result
                            result = 0
                            break
                        }
                        // 不够,留最小。
                        let value = array[i] - minSize
                        array[i] = minSize
                        result -= value
                    }
                    if offset > result + 0.001 {
                        array[current + 1] += (offset - result)
                    }
                    ratios = array
                    return
                }
                
                // 向右或下。
                for i in stride(from: current + 1, to: array.count, by: 1) {
                    if result < 0.001 {
                        break
                    }
                    // 足够。
                    if array[i] > result + minSize  {
                        
                        array[i] -= result
                        result = 0
                        break
                    }
                    // 不够,留最小。
                    let value = array[i] - minSize
                    array[i] = minSize
                    result -= value
                }
                if offset > result + 0.001 {
                    array[current] += (offset - result)
                }
                ratios = array
            }
    }
}

拖动的处理分两步:

  • 首先是添加分割线的拖曳事件,这是一种固定处理,即添加 DragGesture对象。
  • 其次是处理onChanged事件,这里的处理是,向移动的方向遂个处理有可能被缩小的视图,保证每个视图能以最小值显示。处理完成后再处理影响放大的那一个即临近分割线并与移动方向相反的那个。

好,这个也就这些,我是姜友华,下次见

你可能感兴趣的:(五、iWriter 分割视图的实现)