• 用Swift创建一个自定义,可调整的控件


    本文翻译自:HOW TO BUILD A CUSTOM (AND “DESIGNABLE”) CONTROL IN SWIFT

    大约两年前我写了一篇关于如何在iOS里创建自定义控件的教程。那篇教程在开发者社区中非常受欢迎,所以我决定用Swift语言来更新它,同时添加 designale/inspectable 属性的支持,以便直接通过Interface Builder来设计这个控件。

    在我们开始教程之前,让我们看一下最终的效果:

    开始吧!


    不管是你自己设计用户界面还是有设计师帮你,UIKit提供的标准控件都无法完全符合你的需求。

    例如,如果你想创建一个控件让用户选择0到360度的角度呢?

    一个解决方案是创建一个圆形滑块,让用户拖动旋钮选择角度值。这可能是你在很多其它的界面上看到的一种情况,但却不同于UIKit提供的。

    这就是为什么这是一个完美的例子,让我们使用UIKit预留的功能,创建一些特别的东西。

    子类化UIControl


    UIControl 是UIView 的子类,同时也是所有UIKit 控件的父类(例如UIButton,UISlider,UISwitch等等)。 UIControl实例的主要规则就是创建一个逻辑来分发动作到它们的目标上。主要是根据它的状态来绘制特定的用户界面(90%的时间都是在做这个事,例如高亮,选择,禁用)。

    通过UIControl我们管理三个重要的任务:

    * 绘制用户界面

    * 跟踪用户交互

    * 管理Target-Action模式

    在圆形滑块中,我们将创建一个用户界面(滑块本身),用户可以与之交互(移动旋钮)。用户的决定将会被转换成动作传给控件的目标(控件转换这个旋钮的位置到一个0到360度之间的值,同时应用target/action模式)。

    好了,我们准备打开Xcode。我提议你在文章的最后下载完整的项目工程,然后跟着这个教程来阅读我的代码。

    我们将按照上面说的三步来实现。

    这些步骤完全是模块化的,意味着如果你对我绘制这个组件的方法不感兴趣,你可以直接跳到步骤2和步骤3。

    打开 BWCircluarSlider.swift文件,然后跟着下一章。

    1) 绘制用户界面


    我喜欢Core Graphics,我想创建一些你可以继续自定义的东西。我唯一想用UIKit绘制的部分就是那个用来展示滑动值的文本框。

    注意:Core Graphics的一些知识还是必须的,但是你应该还是可以阅读这个代码,我将尽我可能地从头解释。

    让我们分析一下这个控件不同的部分,以便更好地查看它是如何绘制的。

    首先,一个黑色圆环定义了滑块的背景。

    活动的区域会用蓝色到紫色的渐变来填充。

    那个旋钮是让用户拖动来选择值的

    最后,一个文本框来表明当前选择的角度。下一节中它将会接受用户的从键盘输入的值。

    为了绘制这个界面,我门主要使用 drawRect 函数,函数里面的第一步就是获取当前的图形上下文。

    let ctx = UIGraphicsGetCurrentContext()
    

    绘制背景

    背景是由一个360度的圆弧定义的。这可以通过CGContextAddArc 添加右路径到上下文中,然后添加笔画来实现。

    下面的代码是用来实现这个简单的任务的:

    //Build the circle
    CGContextAddArc(ctx, 
                    CGFloat(self.frame.size.width / 2.0),
                    CGFloat(self.frame.size.height / 2.0), 
                    radius, 
                    0, 
                    CGFloat(M_PI * 2), 
                    0)
    
    // Set fill/stroke color
    UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0).set()
    
    // Set line info
    CGContextSetLineWidth(ctx, 72)
    CGContextSetLineCap(ctx, kCGLineCapButt)
    
    // Draw it!
    CGContextDrawPath(ctx, kCGPathStroke)
    

    函数CGContextArc 以圆弧中心坐标和半径开始,还需要用弧度表示的开始和结束的角度(你可以在文件BWCircularSlider.swift开头的地方找到一些数学帮助方法)以及最后一个参数来表明绘制方向,0表示逆时针。

    其它的代码行仅仅是设置,像颜色,行宽。最后我们使用CGContextDrawPath函数绘制这个路径。

    绘制活动区域

    这部分有些技巧。我们绘制一个线性渐变遮罩的图片。让我们看看是如何做的。

    这个遮罩图片像有一个洞穿过去了,所以我们只能看到原始渐变矩形的一部分。

    一个有趣的方面是,这次这个圆弧绘制了一个阴影,创造了一种模糊效果的遮罩。

    创建这个遮罩图片:

    UIGraphicsBeginImageContext(CGSizeMake(self.bounds.size.width,self.bounds.size.height));
    let imageCtx = UIGraphicsGetCurrentContext()
    CGContextAddArc(imageCtx, 
                    CGFloat(self.frame.size.width/2) , 
                    CGFloat(self.frame.size.height/2), 
                    radius, 
                    0, 
                    CGFloat(DegreesToRadians(Double(angle))) ,
                    0);
    UIColor.redColor().set()
    
    //Use shadow to create the Blur effect
    CGContextSetShadowWithColor(imageCtx, CGSizeMake(0, 0), CGFloat(self.angle/15), UIColor.blackColor().CGColor);
    
    //define the path
    CGContextSetLineWidth(imageCtx, Config.TB_LINE_WIDTH)
    CGContextDrawPath(imageCtx, kCGPathStroke)
    
    //save the context content into the image mask
    var mask:CGImageRef = CGBitmapContextCreateImage(UIGraphicsGetCurrentContext());
    UIGraphicsEndImageContext();
    

    首先我们创建一个图片上下文,然后激活阴影。函数CGContextSetShadowWithColor帮我们选择了:

    * 图形上下文

    * 偏移值(我们不需要)

    * 模糊值(我们会参数化这个值,在用户进行交互过程中使用当前角度除以15获得一个在模糊区域上的简单动画效果)

    * 颜色

    我们还是绘制一个圆弧,这次是根据当前的角度。

    例如,如果实例的角度变量等于360度,则绘制一整个圆,如果是90度,我们只是绘制一部分。最后我们使用CGBitmapContextCreateImage函数来从当前的绘制获取一个图片资源。这个图片将是我们的遮罩。

    剪切上下文:

    现在我们有了这个遮罩来定义这个可以看到渐变的“洞”。

    我们使用CGContextClipToMask 函数来剪切上下文,然后传递我们刚刚创建的遮罩。

    CGContextClipToMask(ctx, self.bounds, mask);
    

    最后,我们绘制这个渐变:

    // Split colors in components (rgba)
    let startColorComps:UnsafePointer<CGFloat> = CGColorGetComponents(startColor.CGColor);
    let endColorComps:UnsafePointer<CGFloat> = CGColorGetComponents(endColor.CGColor);
    
    let components : [CGFloat] = [
        startColorComps[0], startColorComps[1], startColorComps[2], 1.0,     // Start color
        endColorComps[0], endColorComps[1], endColorComps[2], 1.0      // End color
    ]
    
    // Setup the gradient
    let baseSpace = CGColorSpaceCreateDeviceRGB()
    let gradient = CGGradientCreateWithColorComponents(baseSpace, components, nil, 2)
    
    // Gradient direction
    let startPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect))
    let endPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect))
    
    // Draw the gradient
    CGContextDrawLinearGradient(ctx, gradient, startPoint, endPoint, 0);
    CGContextRestoreGState(ctx);
    

    绘制这个渐变需要很多的操作,但基本上分为4个部分(用注释分开了):

    * 定义颜色

    * 定义渐变方向

    * 选择一种颜色空间

    * 创建和绘制渐变

    感谢这个遮罩,仅仅这个矩形的一部分会可见。

    绘制旋钮

    现在我们想在当前的角度下的右边绘制这个旋钮。这一步非常简单(我们只是绘制一个白色圆圈),但是需要一些计算来获得这个旋钮的位置。

    我们必须使用三角函数来把一个标量值转换成一个CGPoint。不用害怕,这只是使用了正弦和余弦函数。

    func pointFromAngle(angleInt:Int)->CGPoint{
    
        //Circle center
        let centerPoint = CGPointMake(self.frame.size.width/2.0 - Config.TB_LINE_WIDTH/2.0, self.frame.size.height/2.0 - Config.TB_LINE_WIDTH/2.0);
    
        //The point position on the circumference
        var result:CGPoint = CGPointZero
        let y = round(Double(radius) * sin(DegreesToRadians(Double(-angleInt)))) + Double(centerPoint.y)
        let x = round(Double(radius) * cos(DegreesToRadians(Double(-angleInt)))) + Double(centerPoint.x)
        result.y = CGFloat(y)
        result.x = CGFloat(x)
    
        return result;
    }
    

    给定一个角度,找到圆周上的点,我们同时需要圆心和它的半斤。

    使用正弦函数sin可以获取Y轴上的坐标,使用余弦函数可以获取X轴上的坐标。

    记住,正弦函数和余弦函数返回的值都是假定半径为1的。我们只需要乘以我们的半径值然后移到相对圆心的位置即可。

    我希望下面的函数会帮助你更好地理解:

    point.y = center.y + (radius * sin(angle)); 
    point.x = center.x + (radius * cos(angle));
    

    现在我们知道如何获取旋钮的位置了,可以用下面的函数绘制:

    func drawTheHandle(ctx:CGContextRef){
    
        CGContextSaveGState(ctx);
    
        //I Love shadows
        CGContextSetShadowWithColor(ctx, CGSizeMake(0, 0), 3, UIColor.blackColor().CGColor);
    
        //Get the handle position
        var handleCenter = pointFromAngle(angle)
    
        //Draw It!
        UIColor(white:1.0, alpha:0.7).set();
        CGContextFillEllipseInRect(ctx, CGRectMake(handleCenter.x, handleCenter.y, Config.TB_LINE_WIDTH, Config.TB_LINE_WIDTH));
    
        CGContextRestoreGState(ctx);
    }
    

    上面的步骤是:

    * 保存当前的上下文(当你在一个分开的函数中进行绘制时,保存上下文是一种很好的实践)。

    * 为这个旋钮设置一些阴影。

    * 定义旋钮的颜色,使用函数CGContextFillEllipseInRect绘制。

    我们可以在函数 drawRect函数的最后调用这个函数:

    drawTheHandle(ctx)
    

    我们已经完成了绘制的部分了。

    2) 跟踪用户的交互

    子类化UIControl,我们可以重写3个特别的方法来提供自定义的跟踪行为。

    开始跟踪

    当在一个控件的范围内发生了一个触摸事件时,消息 beginTrackingWithTouch 会首先发送到这个控件。

    让我们看看如何重写它吧:

    override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) -> Bool {
        super.beginTrackingWithTouch(touch, withEvent: event)
    
        return true
    }
    

    它返回一个Bool 值来决定这个控件是否需要在这个触摸移动时响应。对于我们的情况,需要跟踪触摸的移动,所以我们返回true。这个方法有两个参数,一个是触摸对象,一个是事件。

    继续跟踪

    在上一个方法中我们已经指定了我们想跟踪一个持续的事件,所以一个特定的方法: continueTrackingWithTouch 将会在用户执行移动时调用:

    func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) -> Bool
    

    这个方法返回一个Bool值来指定一个触摸跟踪是否应该持续。

    我们可以使用这个函数根据触摸的位置来过滤用户的动作。例如,我们可以选择让触摸的位置在圆圈内时才激活这个控件。但在这里我们的情况是希望处理任何的触摸位置。

    在这个教程里,这个方法负责改变旋钮的位置(我们将会在下面看到在这个方法里发送动作到目标)。

    我们以下面的代码重写:

    override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) -> Bool {
    super.continueTrackingWithTouch(touch, withEvent: event)
    
        let lastPoint = touch.locationInView(self)
    
        self.moveHandle(lastPoint)
    
        self.sendActionsForControlEvents(UIControlEvents.ValueChanged)
    
        return true
    }
    

    首先我们使用locationInView 来获取触摸的位置。然后我们把它传递给moveHandle函数,这个函数会把这个位置转换成一个有效的旋钮的位置。

    什么事有效的位置呢?

    因为这个旋钮应该仅仅在圆圈的区域内移动。但是我们不希望强制用户必须在圆圈内移动手指,因为位置太小了,不好操作,体验会很不好。所以我们将接受任何一个触摸位置,然后转换它到滑块圆圈内。

    moveHandle函数做了这个工作,另外,在这个函数里,我们还把位置转换为角度值。

    func moveHandle(lastPoint:CGPoint){
    
        //Get the center
        let centerPoint:CGPoint  = CGPointMake(self.frame.size.width/2, self.frame.size.height/2);
        //Calculate the direction from a center point and a arbitrary position.
        let currentAngle:Double = AngleFromNorth(centerPoint, p2: lastPoint, flipped: false);
        let angleInt = Int(floor(currentAngle))
    
        //Store the new angle
        angle = Int(360 - angleInt)
    
        //Update the textfield
        textField!.text = "(angle)"
    
        //Redraw
        setNeedsDisplay()
    }
    

    大部分的工作都是由AngleFromNorth来完成的。给定两个点,它会返回角度值:

    func AngleFromNorth(p1:CGPoint , p2:CGPoint , flipped:Bool) -> Double {
        var v:CGPoint  = CGPointMake(p2.x - p1.x, p2.y - p1.y)
        let vmag:CGFloat = Square(Square(v.x) + Square(v.y))
        var result:Double = 0.0
        v.x /= vmag;
        v.y /= vmag;
        let radians = Double(atan2(v.y,v.x))
        result = RadiansToDegrees(radians)
        return (result >= 0  ? result : result + 360.0);
    }
    

    注意,不是我写的这个angleFromNorth方法,我是直接从Apple提供的一个clockControl 例子中拿来的。

    现在我们有了一个表示角度的值,我们保存到angle 属性中,然后更新文本框的值。

    setNeedDisplay 函数确保 drawRect 方法尽快被调用。

    结束跟踪

    下面的方法是当跟踪结束时会触发:

    override func endTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) {
        super.endTrackingWithTouch(touch, withEvent: event)
    }
    

    在这个例子中,我们不需要重写这个方法,但是如果我们想在用户结束与这个控件交互时做些操作,那么这个方法将会很有用。

    3) Target-Action 模式


    到这里,这个圆形滑块就可以工作了。你可以拖动这个旋钮,会看到文本框中的值变化。现在

    给控件事件发送动作

    如果我们想与UIControl的行为一致,我们必须在控件的值发生变化时收到通知。为了实现这个,我们可以使用sendActionsForControlEvents函数来指定事件的类型,这里是UIControlEventValueChanged。

    关于事件的类型还有很多可能的值(在Xcode中,cmd + 鼠标点击UIControlEventValueChanged会看到这个列表值)。例如,如果你的控件是一个UITextField的子类,你可能对UIControlEventEdigitingDidBegin事件感兴趣,或者,如果你想在触摸抬起时收到通知,可以使用UIControlTouchUpInside。

    如果你看回第二小节,你将看到我们在函数continueTrackingWithTouch返回前调用了sendActionsForControlEvents。

    self.sendActionsForControlEvents(UIControlEvents.ValueChanged)
    

    感谢这个方法,当用户移动旋钮时,每一个注册的对象都会收到关于这个变化的通知。

    如何使用这个控件


    现在我们可以在我们的应用中使用这个自定义的控件了。

    因为这个控件是UIControl的子类,它不能直接在 Interface Builder 使用。但是别担心,我们可以使用一个UIView 作为桥梁,然后以这个视图的子视图来访问它。

    打开 BWCircularSliderView.swift 文件来跟着我一起做。查看如下的awakeFromNib 方法:

    override func awakeFromNib() {
    
        super.awakeFromNib()
    
        // Build the slider
        let slider:BWCircularSlider = BWCircularSlider(startColor:self.startColor, endColor:self.endColor, frame: self.bounds)
    
        // Attach an Action and a Target to the slider
        slider.addTarget(self, action: "valueChanged:", forControlEvents: UIControlEvents.ValueChanged)
    
        // Add the slider as subview of this view
        self.addSubview(slider)
    
    }
    

    我们使用 init:startColor:endColor:frame 方法来实例化一个圆形滑块,顺便指定了颜色和大小。

    这是一个自定义的初始化方法,用来保存渐变颜色和一个与桥梁视图大小一样的frame。意味着这个控件将继承这个视图的大小(你同样可以用自动布局来实现)。

    现在我们使用 addTarget:action:forControlEvent: 来定义我们想如何与这个控件进行交互。

    slider.addTarget(self, action: "valueChanged:", forControlEvents: UIControlEvents.ValueChanged)
    

    然后我们可以实现valueChanged方法来在控件值发生变化时做点什么:

    func valueChanged(slider:BWCircularSlider){
        // Do something with the new value...
        println("Value changed (slider.angle)")
    }
    

    现在检查 重写的方法 willMoveToSuperview

    #if TARGET_INTERFACE_BUILDER
      override func willMoveToSuperview(newSuperview: UIView?) {
          let slider:BWCircularSlider = BWCircularSlider(startColor:self.startColor, endColor:self.endColor, frame: self.bounds)
          self.addSubview(slider)
      }
    #else
    

    这段代码,加上@IBInspectable 和 @IBDesignable 关键字可以实现在 Interface Builder中直接预览视图效果(可以查看这个文章来了解更多关于 IBDesignable的知识)。

    (这个 TARGET_INTERFACE_BUILDER 表示我们想要在InterfaceBuilder 中才执行这段代码,而当应用运行时不会执行)。

    现在我们只需要在Storyboard中添加一个BWCircularSliderView实例,直接在Interface Builder中改变 startColor 和 endColor属性值,然后这个控件的预览效果会立刻显示在屏幕上。

    总结


    通过我在这篇教程一开始说的步骤,你可以创建任何你想要的控件。

    当然可能还有很多其它的方式来创建类似的效果,但我还是按照苹果的建议,100%展示给你官方文档中建议的方法。

    下载代码

  • 相关阅读:
    hql 链接查询
    Spring事务配置的五种方式
    web学习:Spring2.5+Hibernate3.3+Struts1.3整合小例子
    Strut2 spring hibernate 整合
    Spring+Struts集成(方案一)
    Remember that ordinal parameters are 1-based!
    ssh传输出现encountered 1 errors during the transfer解决办法
    编程时 对 用途这个字段定义时 不要用using 这个英文
    You can't specify target table 'charge' for update in FROM clause
    Navicat数据存放位置和备份数据库路径设置
  • 原文地址:https://www.cnblogs.com/YungMing/p/4353679.html
Copyright © 2020-2023  润新知