UIKit与Core Graphics绘图技术详解

本文深入探讨了iOS中的Quartz 2D绘图引擎,特别是Core Graphics中的渐变技术。内容包括轴向和径向渐变的创建,对比CGShading和CGGradient对象的使用,以及如何扩展渐变端点的颜色。通过实例展示了如何使用CGGradient和CGShading对象来绘制轴向和径向渐变,详细讲解了绘制过程中的关键步骤,如创建CGFunction对象、裁减上下文、释放对象等。最后,文章还提到了Quartz 2D中的路径绘制,如点、线段、弧和贝塞尔曲线的使用方法。
摘要由CSDN通过智能技术生成

        Core Graphics Framework是一套基于C的API框架,使用了Quartz作为绘图引擎。它提供了低级别、轻量级、高保真度的2D渲染。该框架可以用于基于路径的绘图、变换、颜色管理、脱屏渲染,模板、渐变、遮蔽、图像数据管理、图像的创建、遮罩以及PDF文档的创建、显示和分析。为了从感官上对这些概念做一个入门的认识,你可以运行一下官方的 example code
 
        iOS支持两套图形API族:Core Graphics/QuartZ 2D 和OpenGL ES。OpenGL ES是跨平台的图形API,属于OpenGL的一个简化版本。QuartZ 2D是苹果公司开发的一套API,它是Core Graphics Framework的一部分。需要注意的是:OpenGL ES是应用程序编程接口,该接口描述了方法、结构、函数应具有的行为以及应该如何被使用的语义。也就是说它只定义了一套规范,具体的实现由设备制造商根据规范去做。而往往很多人对接口和实现存在误解。举一个不恰当的比喻:上发条的时钟和装电池的时钟都有相同的可视行为,但两者的内部实现截然不同。因为制造商可以自由的实现Open GL ES,所以不同系统实现的OpenGL ES也存在着巨大的性能差异。
        

        Quartz 2D是iOS和OS X环境下的2D绘图引擎。涉及内容包括:基于路径的绘图,透明度绘图,遮盖,阴影,透明层,颜色管理,防锯齿渲染,生成PDF,以及PDF元数据相关处理。Quartz 2D也被称为Core Graphics,缩写为CG。 Quartz 2D与Core Graphics统称为Quartz。Quartz 2D接口是Core Graphics背后的二维图形库。


一.绘制视图

        如果是自定义视图(UIView),则必须重写drawRect:方法,在此提供相应的绘制代码。

        

        视图的绘制周期:


        在iOS上绘制的时候比较麻烦。首先为需要绘制的视图或视图的部分区域设置一个需要绘制的标志,在事件循环的每一轮中,绘制引擎去检查是否有需要更新的内容,如果有就会调用视图的drawRect:方法进行绘制,因此我们需要绘制的视图中重写drawRect:方法。


        只要iOS任务一个视图需要被刷新或者重绘,drawRect:方法就会被调用。这就意味着,drawRect:的调用频率很高,特别在动画及改变大小、位置的操作过程中,所以应当是极为轻量的。作为开发者,你绝对不应该从你的代码里直接调用drawRect:,也不要在这个方法里分配或者释放内存。


一旦drawRect:方法被调用,就可以使用任何的UIKit、Quartz 2D、OpenGL ES等技术对视图的内容进行绘制了。


        绘图的过程中出了使用drawRect:方法,还有setNeedsDisplay:和setNeedsDisplayInRect:。setNeedsDisplay:和setNeedsDisplayInRect:方法是设置视图或者视图部分区域是否需要重写绘制,setNeedsDisplay:是重新绘制整个视图,setNeedsDisplayInRect:是重新绘制视图的部分区域。


        原则上,尽量不要绘制视图的全部,以减少绘制带来的开销。触发视图重新绘制的动作有如下几种:

1.遮挡你的视图的其他视图被移动或删除操作的时候;

        2.将视图的hidden属性声明设置为NO,使其从隐藏状态变为可见;

        3.将视图滚出屏幕,然后再重新回到屏幕上;

        4.显示调用视图的setNeedsDisplay:或setNeedsDisplayInRect:方法。


        UIView中的绘制和更新视图方法:

- (void)drawRect:(CGRect)rect
- (void)setNeedsDisplay
- (void)setNeedsDisplayInRect:(CGRect)invalidRect

@property(nonatomic) CGFloat contentScaleFactor
        内容比例因子:

        比例因子决定了在视图内容从逻辑坐标空间(以点为单位)到硬件坐标空间(以像素为单位)的映射。这个值通常是1.0或2.0。例如,如果定标系数是2.0,并且视图帧大小为50×50点(points),用于呈现该内容的位图的大小为100×100像素(pixels)。



        setNeedsDisplay和setNeedsLayout:

        1.UIView的setNeedsDisplay和setNeedsLayout方法

        首先两个方法都是异步执行的。而setNeedsDisplay会调用自动调用drawRect方法,这样可以拿到  UIGraphicsGetCurrentContext,就可以画画了。而setNeedsLayout会默认调用layoutSubViews,就可以  处理子视图中的一些数据。

        综上所诉,setNeedsDisplay方便绘图,而layoutSubViews方便出来数据。

        layoutSubviews在以下情况下会被调用:

        1.init初始化不会触发layoutSubviews。
        2.addSubview会触发layoutSubviews。
        3.设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。
        4.滚动一个UIScrollView会触发layoutSubviews。
        5.旋转Screen会触发父UIView上的layoutSubviews事件。
        6.改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
        7.直接调用setLayoutSubviews。

        drawRect在以下情况下会被调用:

         1.如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect调用是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的.所以不用担心在控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量值).

        2.该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
        3.通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
        4.直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。

        以上1,2推荐;而3,4不提倡

        drawRect方法使用注意点:
        1.若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或者 setNeedsDisplayInRect,让系统自动调该方法。
        2.若使用calayer绘图,只能在drawInContext: 中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法
        3.若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来掉用setNeedsDisplay实时刷新屏幕

        

二.图形上下文


        在调用drawRect:方法之前,视图对象会自动配置起绘制环境,是代码立即执行进行绘制。作为这些配置的一部分,UIView对象会为当前绘制环境创建一个图形上下文(对应于CGContextRef封装类型)。在前面的实例中就是采用这种默认方式的图形上下文。


        我们也可以在drawRect:方法中通过(CGContextRef)UIGraphicsGetCurrentContext(void)函数获得访问图形上下文对象。图形上下文仅对当前的drawRect:方法调用有效,不要把图形上下文对象设置为成员变量。


        Core Graphics API所有的操作都在一个上下文中进行。所以在绘图之前需要获取该上下文并传入执行渲染的函数中。如果你正在渲染一副在内存中的图片,此时就需要传入图片所属的上下文。获得一个图形上下文是我们完成绘图任务的第一步,你可以将图形上下文理解为一块画布。如果你没有得到这块画布,那么你就无法完成任何绘图操作。当然,有许多方式获得一个图形上下文,这里我介绍两种最为常用的获取方法。
 
        第一种方法就是创建一个图片类型的上下文。调用UIGraphicsBeginImageContextWithOptions函数就可获得用来处理图片的图形上下文。利用该上下文,你就可以在其上进行绘图,并生成图片。调用UIGraphicsGetImageFromCurrentImageContext函数可从当前上下文中获取一个UIImage对象。记住在你所有的绘图操作后别忘了调用UIGraphicsEndImageContext函数关闭图形上下文。
 
        第二种方法是利用cocoa为你生成的图形上下文。当你子类化了一个UIView并实现了自己的drawRect:方法后,一旦drawRect:方法被调用,Cocoa就会为你创建一个图形上下文,此时你对图形上下文的所有绘图操作都会显示在UIView上。
 
        判断一个上下文是否为当前图形上下文需要注意的几点:
        1.UIGraphicsBeginImageContextWithOptions函数不仅仅是创建了一个适用于图形操作的上下文,并且该上下文也属于当前上下文。
        2.当drawRect方法被调用时,UIView的绘图上下文属于当前图形上下文。
        3.回调方法所持有的context:参数并不会让任何上下文成为当前图形上下文。此参数仅仅是对一个图形上下文的引用罢了。
 
        作为初学者,很容易被UIKit和Core Graphics两个支持绘图的框架迷惑。
 
        UIKit

        像UIImage、NSString(绘制文本)、UIBezierPath(绘制形状)、UIColor都知道如何绘制自己。这些类提供了功能有限但使用方便的方法来让我们完成绘图任务。一般情况下,UIKit就是我们所需要的。

        使用UiKit,你只能在当前上下文中绘图,所以如果你当前处于UIGraphicsBeginImageContextWithOptions函数或drawRect:方法中,你就可以直接使用UIKit提供的方法进行绘图。如果你持有一个context:参数,那么使用UIKit提供的方法之前,必须将该上下文参数转化为当前上下文。幸运的是,调用UIGraphicsPushContext 函数可以方便的将context:参数转化为当前上下文,记住最后别忘了调用UIGraphicsPopContext函数恢复上下文环境。
 
        Core Graphics

        这是一个绘图专用的API族,它经常被称为QuartZ或QuartZ 2D。Core Graphics是iOS上所有绘图功能的基石,包括UIKit。
 
        使用Core Graphics之前需要指定一个用于绘图的图形上下文(CGContextRef),这个图形上下文会在每个绘图函数中都会被用到。如果你持有一个图形上下文context:参数,那么你等同于有了一个图形上下文,这个上下文也许就是你需要用来绘图的那个。如果你当前处于UIGraphicsBeginImageContextWithOptions函数或drawRect:方法中,并没有引用一个上下文。为了使用Core Graphics,你可以调用UIGraphicsGetCurrentContext函数获得当前的图形上下文。
 
        至此,我们有了两大绘图框架的支持以及三种获得图形上下文的方法(drawRect:、drawRect: inContext:、UIGraphicsBeginImageContextWithOptions)。那么我们就有6种绘图的形式。如果你有些困惑了,不用怕,我接下来将说明这6种情况。无需担心还没有具体的绘图命令,你只需关注上下文如何被创建以及我们是在使用UIKit还是Core Graphics。
 
        第一种绘图形式:在UIView的子类方法drawRect:中绘制一个蓝色圆,使用UIKit在Cocoa为我们提供的当前上下文中完成绘图任务。
 
 
 
 
  1. - (void) drawRect: (CGRect) rect { 
  2.  
  3. UIBezierPath* p = [UIBezierPathbezierPathWithOvalInRect:CGRectMake(0,0,100,100)]; 
  4.  
  5. [[UIColor blueColor] setFill]; 
  6.  
  7. [p fill]; 
  8.  
 
        第二种绘图形式:使用Core Graphics实现绘制蓝色圆。
 
 
 
 
  1. - (void) drawRect: (CGRect) rect { 
  2.  
  3. CGContextRef con = UIGraphicsGetCurrentContext(); 
  4.  
  5. CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100)); 
  6.  
  7. CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor); 
  8.  
  9. CGContextFillPath(con); 
  10.  
 
        第三种绘图形式:我将在UIView子类的drawLayer:inContext:方法中实现绘图任务。drawLayer:inContext:方法是一个绘制图层内容的代理方法。为了能够调用drawLayer:inContext:方法,我们需要设定图层的代理对象。但要注意,不应该将UIView对象设置为显示层的委托对象,这是因为UIView对象已经是隐式层的代理对象,再将它设置为另一个层的委托对象就会出问题。轻量级的做法是:编写负责绘图形的代理类。在MyView.h文件中声明如下代码:
 
 
 
 
  1. @interface MyLayerDelegate : NSObject 
  2.  
  3. @end 

        然后MyView.m文件中实现接口代码:
 
 
 
 
  1. @implementation MyLayerDelegate 
  2.  
  3. - (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)ctx { 
  4.  
  5.   UIGraphicsPushContext(ctx); 
  6.  
  7.   UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)]; 
  8.  
  9.   [[UIColor blueColor] setFill]; 
  10.  
  11.   [p fill]; 
  12.  
  13.   UIGraphicsPopContext(); 
  14.  
  15.  
  16. @end 
 
        直接将代理类的实现代码放在MyView.m文件的#import代码的下面,这样感觉好像在使用私有类完成绘图任务(虽然这不是私有类)。需要注意的是,我们所引用的上下文并不是当前上下文,所以为了能够使用UIKit,我们需要将引用的上下文转变成当前上下文。
 
        因为图层的代理是assign内存管理策略,那么这里就不能以局部变量的形式创建MyLayerDelegate实例对象赋值给图层代理。这里选择在MyView.m中增加一个实例变量,因为实例变量默认是strong:
 
 
 
 
  1. @interface MyView () { 
  2.  
  3. MyLayerDelegate* _layerDeleagete; 
  4.  
  5.  
  6. @end 
 
        使用该图层代理:
 
 
 
 
  1. MyView *myView = [[MyView alloc] initWithFrame: CGRectMake(0, 0, 320, 480)]; 
  2.  
  3. CALayer *myLayer = [CALayer layer]; 
  4.  
  5. _layerDelegate = [[MyLayerDelegate alloc] init]; 
  6.  
  7. myLayer.delegate = _layerDelegate; 
  8.  
  9. [myView.layer addSublayer:myLayer]; 
  10.  
  11. [myView setNeedsDisplay]; // 调用此方法,drawLayer: inContext:方法才会被调用。 
 
        第四种绘图形式: 使用Core Graphics在drawLayer:inContext:方法中实现同样操作,代码如下:
 
 
 
 
  1. - (void)drawLayer:(CALayer*)lay inContext:(CGContextRef)con { 
  2.  
  3. CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100)); 
  4.  
  5. CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor); 
  6.  
  7. CGContextFillPath(con); 
  8.  

        最后,演示UIGraphicsBeginImageContextWithOptions的用法,并从上下文中生成一个UIImage对象。生成UIImage对象的代码并不需要等待某些方法被调用后或在UIView的子类中才能去做。
 
        第五种绘图形式: 使用UIKit实现:
 
 
 
 
  1. UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0); 
  2.  
  3. UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)]; 
  4.  
  5. [[UIColor blueColor] setFill]; 
  6.  
  7. [p fill]; 
  8.  
  9. UIImage* im = UIGraphicsGetImageFromCurrentImageContext(); 
  10.  
  11. UIGraphicsEndImageContext(); 

        解释一下UIGraphicsBeginImageContextWithOptions函数参数的含义:第一个参数表示所要创建的图片的尺寸;第二个参数用来指定所生成图片的背景是否为不透明,如上我们使用YES而不是NO,则我们得到的图片背景将会是黑色,显然这不是我想要的;第三个参数指定生成图片的缩放因子,这个缩放因子与UIImage的scale属性所指的含义是一致的。传入0则表示让图片的缩放因子根据屏幕的分辨率而变化,所以我们得到的图片不管是在单分辨率还是视网膜屏上看起来都会很好。
 
        第六种绘图形式: 使用Core Graphics实现:
 
 
 
 
  1. UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0); 
  2.  
  3. CGContextRef con = UIGraphicsGetCurrentContext(); 
  4.  
  5. CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100)); 
  6.  
  7. CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor); 
  8.  
  9. CGContextFillPath(con); 
  10.  
  11. UIImage* im = UIGraphicsGetImageFromCurrentImageContext(); 
  12.  
  13. UIGraphicsEndImageContext(); 
 
         UIKit和Core Graphics可以在相同的图形上下文中混合使用。在iOS 4.0之前,使用UIKit和UIGraphicsGetCurrentContext被认为是线程不安全的。而在iOS4.0以后苹果让绘图操作在第二个线程中执行解决了此问题。

         Core Graphics上下文属性设置
         当你在图形上下文中绘图时,当前图形上下文的相关属性设置将决定绘图的行为与外观。因此,绘图的一般过程是先设定好图形上下文参数,然后绘图。比方说,要画一根红线,接着画一根蓝线。那么首先需要将上下文的线条颜色属性设定为为红色,然后画红线;接着设置上下文的线条颜色属性为蓝色,再画出蓝线。表面上看,红线和蓝线是分开的,但事实上,在你画每一条线时,线条颜色却是整个上下文的属性。无论你用的是UIKit方法还是Core Graphics函数。
 
         因为图形上下文在每一时刻都有一个确定的状态,该状态概括了图形上下文所有属性的设置。为了便于操作这些状态,图形上下文提供了一个用来持有状态的栈。调用CGContextSaveGState函数,上下文会将完整的当前状态压入栈顶;调用CGContextRestoreGState函数,上下文查找处在栈顶的状态,并设置当前上下文状态为栈顶状态。
 
         因此一般绘图模式是:在绘图之前调用CGContextSaveGState函数保存当前状态,接着根据需要设置某些上下文状态,然后绘图,最后调用CGContextRestoreGState函数将当前状态恢复到绘图之前的状态。要注意的是,CGContextSaveGState函数和CGContextRestoreGState函数必须成对出现,否则绘图很可能出现意想不到的错误,这里有一个简单的做法避免这种情况。代码如下:
 
  
  
  
  1. - (void)drawRect:(CGRect)rect { 
  2.  
  3. CGContextRef ctx = UIGraphicsGetCurrentContext(); 
  4.  
  5. CGContextSaveGState(ctx); 
  6.  
  7.  
  8. // 绘图代码 
  9.  
  10.  
  11. CGContextRestoreGState(ctx); 
  12.  
  13.     }   
 
         但你不需要在每次修改上下文状态之前都这样做,因为你对某一上下文属性的设置并不一定会和之前的属性设置或其他的属性设置产生冲突。你完全可以在不调用保存和恢复函数的情况下先设置线条颜色为红色,然后再设置为蓝色。但在一定情况下,你希望你对状态的设置是可撤销的,我将在接下来讨论这样的情况。
 
         许多的属性组成了一个图形上下文状态,这些属性设置决定了在你绘图时图形的外观和行为。下面我列出了一些属性和对应修改属性的函数;虽然这些函数是关于Core Graphics的,但记住,实际上UIKit同样是调用这些函数操纵上下文状态。



三.UIKit填充与描边


         UIKit提供非常基本的绘图功能,主要的API有:

        1.UIRectFill(CGRect rect),填充矩形函数;

        2.UIRectFrame(CGRect rect),填充描边函数;

        3.UIBezierPath,绘制常见路径类,包括线段、弧线、矩形、矩形、圆角矩形和椭圆的方法。


        例如:

- (void)drawRect:(CGRect)rect
{
[[UIColor browColor] setFill];//为当前的图形上下文设置要填充的颜色
UIRectFill(rect);//按照刚才设置的颜色进行填充矩形
[[UIColor whiteColor] setStroke];//设置图形上下文白色描边
CGRect frame = CGRectMake(20,30,100,300);
UIRectFrame(frame);
}

       

        UIRectFill(CGRect rect):向当前绘图环境所创建的内存中的图片上填充一个矩形。

        UIRectFillUsingBlendMode(CGRect rect , CGBlendMode blendMode):向当前绘图环境所创建的内存中的图片上填充一个矩形,绘制使用指定的混合模式。

        UIRectFrame(CGRect rect):向当前绘图环境所创建的内存中的图片上绘制一个矩形边框。

        UIRectFrameUsingBlendMode(CGRect rect , CGBlendMode blendMode):向当前绘图环境所创建的内存中的图片上绘制一个矩形边框,绘制使用指定的混合模式。


        上面4个方法都是直接绘制在当前绘图环境所创建的内存中的图片上,因此,这些方法都不需要传入CGContextRef作为参数。

       

         UIKit虽然提供了UIBezierPath等类,但是对于线段、渐变、阴影、反锯齿等高性能特性支持还是不及Quartz 2D。


四.UIKit绘制图像


        UIImage类中绘制图像主要的方法:

- (void)drawAtPoint:(CGPoint)point
        设置绘制定点。

- (void)drawAtPoint:(CGPoint)point
          blendMode:(CGBlendMode)blendMode
              alpha:(CGFloat)alpha
        设置绘制定点,并使用自定义混合选项。

- (void)drawInRect:(CGRect)rect
        图片绘制在指定的矩形里。

- (void)drawInRect:(CGRect)rect
         blendMode:(CGBlendMode)blendMode
             alpha:(CGFloat)alpha
        图片绘制在指定的矩形里,并使用自定义混合选项。

- (void)drawAsPatternInRect:(CGRect)rect
        在指定的矩形里绘制图片,如果图片大小超出了指定矩形,形式上与-drawAtPoint:方法类似了,如果图片大小小于指定的矩形,就会有平铺的效果。


五.绘制文本



- (void)drawAtPoint:(CGPoint)point withAttributes:(NSDictionary *)attrs
文本在指定点绘制。

- (void)drawInRect:(CGRect)rect withAttributes:(NSDictionary *)attrs
文本在指定的矩形里绘制。





六.绘制渐变


Quartz提供了两个不透明数据odgago创建渐变:CGShadingRef和CGGradientRef。我们可以使用任何一个来创建轴向(axial)或径向(radial)渐变。一个渐变是从一个颜色到另外一种颜色的填充。

一个轴向渐变(也称为线性渐变)沿着由两个端点连接的轴线渐变。所有位于垂直于轴线的某条线上的点都具有相同的颜色值。

一个径向渐变也是沿着两个端点连接的轴线渐变,不过路径通常由两个圆来定义。

本章提供了一些我们使用Quartz能够创建的轴向和径向渐变的类型的例子,并比较绘制渐变的两种方法,然后演示了如何使用每种不透明数据类型来创建渐变。

轴向和径向渐变实例

Quartz函数提供了一个丰富的功能来创建渐变效果。这一部分显示了一些我们能达到的效果。图8-1中的轴向渐变由橙色向黄色渐变。在这个例子中,渐变轴相对于原点倾斜了45度角。

Figure 8-1 An axial gradient along a 45 degree axis

image

Quartz也允许我们指定一系列的颜色和位置值,以沿着轴来创建更复杂的轴向渐变,如图8-2所示。起始点的颜色值是红色,结束点的颜色是紫罗兰色。同时,在轴上有五个位置,它们的颜色值分别被设置为橙、黄、绿、蓝和靛蓝。我们可以把它看成沿着同一轴线的六段连续的线性渐变。虽然这里的轴线与图8-1是一样的,但这不是必须的。轴线的角度由我们提供的两个端点定义。

Figure 8-2 An axial gradient created with seven locations and colors

image

图8-3显示了一个径向渐变,它从一个小的明亮的红色圆渐变到一个大小黑色的圆。

Figure 8-3 A radial gradient that varies between two circles

image

使用Quartz,我们不局限于创建颜色值改变的渐变;我们可以只修改alpha值,或者创建alpha值与其它颜色组件一起改变的渐变。图8-4显示了一个渐变,其红、绿、蓝组件的值是不变的,但alpha值从1.0渐变到0.1。

注意:如果我们使用alpha值来改变一个渐变,则在绘制一个PDF内容时我们不能捕获这个渐变。因此,这样的渐变无法打印。如果需要绘制一个渐变到PDF,则需要让alpha值为1.0。

Figure 8-4 A radial gradient created by varying only the alpha component

image

我们可以把一个圆放置到一个径向渐变中来创建各种形状。如果一个圆是另一个的一部分或者完全在另一个的外面,则Quartz创建了圆锥和一个圆柱。径向渐变的一个通常用法就是创建一个球体阴影,如图8-5所示。在这种情况下,一个单一的点(半径为0的圆)位于一个大圆以内。

Figure 8-5 A radial gradient that varies between a point and a circle

image

我们可以像图8-6一样通过内嵌几个径向渐变来创建更复杂的效果。它使用同心圆来创建图形中的各环形部分。

Figure 8-6 Nested radial gradients

image

CGShading和CGGradient对象的对比

我们有两个对象类型用于创建渐变,你可能想知道哪一个更好用。本节就来回答这个问题。

CGShadingRef这个不透明数据类型给我们更多的控制权,以确定如何计算每个端点的颜色。在我们创建CGShading对象之前,我们必须创建一个CGFunction对象(CGFunctionRef),这个对象定义了一个用于计算渐变颜色的函数。写一个自定义的函数让我们能够创建平滑的渐变,如图8-3,8-3和8-5及更多非传统的效果,如图8-12所示。

当创建一个CGShading对象时,我们指定其是轴向还是径向。除了计算函数外,我们还需要提供一个颜色空间、起始点和结束点或者是半径,这取决于是绘制轴向还是径向渐变。在绘制时,我们只是简单地传递CGShading对象及绘制上下文给CGContextDrawShading函数。Quartz为渐变上的每个点调用渐变计算函数。

一个CGGradient对象是CGShading对象的子集,其更易于使用。CGGradientRef不透明类型易于作用,因为Quartz在渐变的每一个顶点上计算颜色值。我们不需要提供一个渐变计算函数。当创建一个渐变对象时,我们提供一个位置和颜色的数组。Quartz使用对应的颜色值来计算每个梯度的渐变,。我们可以使用单一的起始与结束点来设置一个渐变对象,如图8-1所示,或者提供一组端点来创建一个类似于图8-2的的效果。使用CGShading对象可以提供多于两个位置的能力。

当我们创建一个CGGradient对象时,我们需要设置一个颜色空间、位置、和每个位置对应的颜色值。当使用一个渐变对象绘制上下文时,我们指定Quartz是绘制一个轴向还是径向渐变。在绘制时,我们指定开始结束点或半径,这取决于我们是绘制轴向还是径向渐变。而CGShading的几何形状是在创建时定义的,而不是绘制时。

表8-1总结了两种不透明数据类型之间的区别。

image

扩展渐变端点外部的颜色

当我们创建一个渐变时,我们可以选择使用纯色来填充渐变端点外部的空间。Quartz使用使用渐变边界上的颜色作为填充颜色。我们可以扩展渐变起点、终点或两端的颜色。我们可以扩展使用CGShading对象或CGGradient对象创建的轴向或径向渐变。

图8-7演示了一个轴向渐变,它扩展了起点和终点两侧的区域。图片中的线段显示了渐变的轴线。我们可以看到,填充颜色与起点和终点的颜色是对应的。

Figure 8-7 Extending an axial gradient

image

图8-8对比了一个未使用扩展的径向渐变和一个在起点和终点两侧使用扩展的径向渐变。Quartz获取了起点和终点的颜色值,并使用这边纯色值来扩展立面。

Figure 8-8 Extending a radial gradient

image

使用CGGradient对象

一个CGGradient对象是一个渐变的抽象定义—它简单地指定了颜色值和位置,但没有指定几何形状。我们可以在轴向和径向几何形状中使用这个对象。作为一个抽象定义,CGGradient对象可能比CGShading对象更容易重用。没有将几何形状存储在CGGradient对象中,这样允许我们使用相同的颜色方案来绘制不同的几何图形,而不需要为多个图形创建多个CGGradient对象。

因为Quartz为我们计算渐变,使用一个CGGradient对象来创建和绘制一个渐变则更直接,只需要以下几步:

  1. 创建一个CGGradient对象,提供一个颜色空间,一个饱含两个或更多颜色组件的数组,一个包含两个或多个位置的数组,和两个数组中元素的个数。
  2. 调用CGContextDrawLinearGradient或CGContextDrawRadialGradient函数并提供一个上下文、一个CGGradient对象、绘制选项和开始结束几何图形来绘制渐变。
  3. 当不再需要时释放CGGradient对象。

一个位置是一个值区间在0.0到1.0之间的CGFloat值,它指定了沿着渐变的轴线的标准化距离。值0.0指定的轴线的起点,1.0指定了轴线的终点。其它的值指定了一个距离的比例。最低限度情况下,Quartz使用两个位置值。如果我们传递NULL值作为位置数组参数,则Quartz使用0作为第一个位置,1作为第二个位置。

每个颜色的颜色组件的数目取决于颜色空间。对于离屏绘制,我们使用一个RGB颜色空间。因为Quartz使用alpha来绘制,每个离屏颜色都有四个组件—红、绿、蓝和alpha。所以,对于离屏绘制,我们提供的颜色组件数组的元素的数目必须是位置数目的4倍。Quartz的RGBA颜色组件可以在0.0到1.0之间改变。

代码清单8-1是创建一个CGGradient对象的代码片断。在声明了必须的变量后,代码设置了位置和颜色组件数组。然后创建了一个通用的RGB颜色空间。(在iOS中,不管RGB颜色空间是否可用,我们都应该调用CGColorSpaceCreateDeviceRGB)。然后,它传递必要的参数到CGGradientCreateWithColorComponents函数。我们同样可以使用CGGradientCreateWithColors,如果我们的程序设置了CGColor对象,这是一种便捷的方法。

Listing 8-1 Creating a CGGradient object

CGGradientRef myGradient;
CGColorSpaceRef myColorspace;
size_t num_locations = 2;
CGFloat locations[2] = { 0.0, 1.0 };
CGFloat components[8] = { 1.0, 0.5, 0.4, 1.0,  // Start color
                          0.8, 0.8, 0.3, 1.0 }; // End color

myColorspace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
myGradient = CGGradientCreateWithColorComponents (myColorspace, components,
                          locations, num_locations);

在创建了CGGradient对象后,我们可以使用它来绘制一个轴向或线性渐变。代码清单8-2声明并设置了线性渐变的起始点然后绘制渐变。图8-1显示了结果。代码没有演示如何获取CGContext对象。

Listing 8-2 Painting an axial gradient using a CGGradient object

CGPoint myStartPoint, myEndPoint;
myStartPoint.x = 0.0;
myStartPoint.y = 0.0;
myEndPoint.x = 1.0;
myEndPoint.y = 1.0;
CGContextDrawLinearGradient (myContext, myGradient, myStartPoint, myEndPoint, 0);

代码清单8-3使用代码清单8-1中创建的CGGradient对象来绘制图8-9中径向渐变。这个例子同时也演示了使用纯色来填充渐变的扩展区域。

Listing 8-3 Painting a radial gradient using a CGGradient object

CGPoint myStartPoint, myEndPoint;
CGFloat myStartRadius, myEndRadius;
myStartPoint.x = 0.15;
myStartPoint.y = 0.15;
myEndPoint.x = 0.5;
myEndPoint.y = 0.5;
myStartRadius = 0.1;
myEndRadius = 0.25;
CGContextDrawRadialGradient (myContext, myGradient, myStartPoint,
                         myStartRadius, myEndPoint, myEndRadius,
                         kCGGradientDrawsAfterEndLocation);

Figure 8-9 A radial gradient painted using a CGGradient object

image

图8-4中的径向渐变使用代码清单8-4中的变量来创建。

Listing 8-4 The variables used to create a radial gradient by varying alpha

CGPoint myStartPoint, myEndPoint;
CGFloat myStartRadius, myEndRadius;
myStartPoint.x = 0.2;
myStartPoint.y = 0.5;
myEndPoint.x = 0.65;
myEndPoint.y = 0.5;
myStartRadius = 0.1;
myEndRadius = 0.25;
size_t num_locations = 2;
CGFloat locations[2] = { 0, 1.0 };
CGFloat components[8] = { 0.95, 0.3, 0.4, 1.0,
                          0.95, 0.3, 0.4, 0.1 };

代码清单8-5显示了用于创建图8-10中的灰色渐变的变量,其中有3个位置。

Listing 8-5 The variables used to create a gray gradient

size_t num_locations = 3;
CGFloat locations[3] = { 0.0, 0.5, 1.0};
CGFloat components[12] = {  1.0, 1.0, 1.0, 1.0,
                            0.5, 0.5, 0.5, 1.0,
                            1.0, 1.0, 1.0, 1.0 };

Figure 8-10 An axial gradient with three locations

image

使用CGShading对象

我们通过调用函数CGShadingCreateAxial或CGShadingCreateRadial创建一个CGShading对象来设置一个渐变,调用这些函数需要提供以下参数:

  1. CGColorSpace对象:颜色空间
  2. 起始点和终点。对于轴向渐变,有轴线的起始点和终点的坐标。对于径向渐变,有起始圆和终点圆中心的坐标。
  3. 用于定义渐变区域的圆的起始半径与终止半径。
  4. 一个CGFunction对象,可以通过CGFunctionCreate函数来获取。这个回调例程必须返回绘制到特定点的颜色值。
  5. 一个布尔值,用于指定是否使用纯色来绘制起始点与终点的扩展区域。

我们提供给CGShading创建函数的CGFunction对象包含一个回调结构体,及Quartz需要实现这个回调的所有信息。也许设置CGShasing对象的最棘手的部分是创建CGFunction对象。当我们调用CGFunctionCreate函数时,我们提供以下参数:

  1. 指向回调所需要的数据的指针
  2. 回调的输入值的个数。Quartz要求回调携带一个输入值。
  3. 一个浮点数的数组。Quartz只会提供数组中的一个元素给回调函数。一个转入值的范围是0(渐变的开始点的颜色)到1(渐变的结束点的颜色)。
  4. 回调提供的输出值的数目。对于每一个输入值,我们的回调必须为每个颜色组件提供一个值,以及一个alpha值来指定透明度。颜色组件值由Quartz提供的颜色空间来解释,并会提供给CGShading创建函数。例如,如果我们使用RGB颜色空间,则我们提供值4作为输出值(R,G,B,A)的数目。
  5. 一个浮点数的数组,用于指定每个颜色组件的值及alpha值。
  6. 一个回调数据结构,包含结构体的版本(设置为0)、生成颜色组件值的回调、一个可选的用于释放回调中info参数表示的数据。该回调类似于以下格式:

    void myCalculateShadingValues (void info, const CGFloat in, CGFloat *out)

在创建CGShading对象后,如果需要我们可以设置额外的裁减操作。然后调用CGContextDrawShading函数来使用渐变来绘制上下文的裁减区域。当调用这个函数时,Quartz调用回调函数来获取从起点到终点这个范围内的颜色值。

当不再需要CGShading对象时,我们调用CGShadingRelease来释放它。

下面我们将一步步地通过代码来看看如何使用CGShading对象来绘制渐变。

使用CGShading对象绘制一个轴向渐变

绘制轴向和径向渐变的步骤是差不多的。这个例子演示了如何使用一个CGShading对象来绘制一个轴向渐变,并在图形上下文中绘制一个半圆形的裁减路径,然后将渐变绘制到裁减区域来达到图8-11的效果。

Figure 8-11 An axial gradient that is clipped and painted

image

为了绘制图中的轴向渐变,需要按以下步骤来处理:

  1. 设置CGFunction对象来计算颜色值
  2. 创建轴向渐变的CGShading对象
  3. 裁减上下文
  4. 使用CGShading对象来绘制轴向渐变
  5. 释放对象
设置CGFunction对象来计算颜色值

我们可以以我们想要的方式来计算颜色值,我们的颜色计算函数包含以下三个参数:

  1. void *info:这个值可以为NULL或者是一个指向传递给CGShading创建函数的数据。
  2. const CGFloat *in:Quartz传递in数组给回调。数组中的值必须在为CGFunction对象定义的输入值范围内。例如,输入范围是0到1;看代码清单8-7
  3. CGFloat *out:我们的回调函数传递out数组给Quartz。它包含用于颜色空间中每个颜色组件的元素及一个alpha值。输出值应该在CGFunction对象中定义的输出值的范围内,例如,输出范围是0到1;看代码清单8-7。

更多关于参数的信息可以查看CGFunctionEvaluateCallback。

代码清单8-6演示了一个函数,它通过将一个常数数组中的值乘以输入值来计算颜色组件值。因为输入值在0到1之间,所以输入值位于黑色(对于RGB来说值为0, 0, 0)和紫色(1, 0, 0.5)之间。注意最后一个组件通常设置为1,表示颜色总是完全不透明的。

Listing 8-6 Computing color component values

static void myCalculateShadingValues (void *info,
              const CGFloat *in,
              CGFloat *out)
{
  CGFloat v;
  size_t k, components;
  static const CGFloat c[] = {
   1, 0, .5, 0 };

  components = (size_t)info;

  v = *in;
  for (k = 0; k < components -1; k++)
    *out++ = c[k] * v;
   *out++ = 1;
}

在写完回调计算颜色值后,我们将其打包以作为CGFunction对象的一部分。代码清单显示了一个函数,它创建了一个包含代码清单8-6中的回调函数的CGFunction对象。

Listing 8-7 Creating a CGFunction object

static CGFunctionRef myGetFunction (CGColorSpaceRef colorspace)
{
    size_t numComponents;
    static const CGFloat input_value_range [2] = { 0, 1 };
    static const CGFloat output_value_ranges [8] = { 0, 1, 0, 1, 0, 1, 0, 1 };
    static const CGFunctionCallbacks callbacks = { 0,
      &myCalculateShadingValues,
      NULL };

    numComponents = 1 + CGColorSpaceGetNumberOfComponents (colorspace);
    return CGFunctionCreate ((void *) numComponents,
      1, 
      input_value_range, 
      numComponents, 
      output_value_ranges, 
      &callbacks);
}
创建一个轴向渐变的CGShading对象

为了创建一个CGShading对象,我们调用CGShadingCreateAxial函数,如代码清单8-8所示。我们传递一个颜色空间,开始点和结束点,一个CGFunction对象,和一个用于指定是否填充渐变的开始点和结束点扩展的布尔值。

Listing 8-8 Creating a CGShading object for an axial gradient

CGPoint     startPoint,
  endPoint;
CGFunctionRef myFunctionObject;
CGShadingRef myShading;

startPoint = CGPointMake(0,0.5);
endPoint = CGPointMake(1,0.5);
colorspace = CGColorSpaceCreateDeviceRGB();
myFunctionObject = myGetFunction (colorspace);

myShading = CGShadingCreateAxial (colorspace,
    startPoint, endPoint,
    myFunctionObject,
    false, false);
裁减上下文

当绘制一个渐变时,Quartz填充当前上下文。绘制一个渐变与操作颜色和模式不同,后者是用于描边或填充一个路径对象。因此,如果要我们的渐变出现在一个特定形状中,我们需要裁减上下文。代码清单8-9的代码添加了一个半圆形到当前上下文,以便渐变绘制到这个裁减区域,如图8-11。

如果我们仔细看,会发现代码绘制的是一个半圆,而图中显示的是一个半椭圆形。为什么呢?我们会看到,当我们查看后面完整的绘制代码时,上下文被缩放了。稍后会详细说明。虽然我们不需要使用缩放或裁减,这些在Quartz 2D中的选项可以帮助我们达到有趣的效果。

Listing 8-9 Adding a semicircle clip to the graphics context

CGContextBeginPath (myContext);
CGContextAddArc (myContext, .5, .5, .3, 0,
                my_convert_to_radians (180), 0);
CGContextClosePath (myContext);
CGContextClip (myContext);
使用CGShading对象来绘制轴向渐变

调用函数CGContextDrawShading使用CGShading对象为指定的颜色渐变来填充当前上下文:

CGContextDrawShading (myContext, myShading);
释放对象

当我们不再需要CGShading对象时,可以调用函数CGShadingRelease来释放它。我们需要同时释放CGColorSpace对象和CGFunction对象,如代码清单8-10所示:

Listing 8-10 Releasing objects

CGShadingRelease (myShading);
CGColorSpaceRelease (colorspace);
CGFunctionRelease (myFunctionObject);
使用CGShading对象绘制轴向渐变的完整例程

代码清单8-11显示了绘制一个轴向渐变的完整例程,使用8-7中的CGFunction对象和8-6中的回调函数。

Listing 8-11 Painting an axial gradient using a CGShading object

void myPaintAxialShading (CGContextRef myContext,
              CGRect bounds)
{
  CGPoint	 startPoint,
        endPoint;
  CGAffineTransform myTransform;
  CGFloat width = bounds.size.width;
  CGFloat height = bounds.size.height;


  startPoint = CGPointMake(0,0.5); 
  endPoint = CGPointMake(1,0.5);

  colorspace = CGColorSpaceCreateDeviceRGB();
  myShadingFunction = myGetFunction(colorspace);

  shading = CGShadingCreateAxial (colorspace, 
                 startPoint, endPoint,
                 myShadingFunction,
                 false, false);

  myTransform = CGAffineTransformMakeScale (width, height);
  CGContextConcatCTM (myContext, myTransform);
  CGContextSaveGState (myContext);

  CGContextClipToRect (myContext, CGRectMake(0, 0, 1, 1));
  CGContextSetRGBFillColor (myContext, 1, 1, 1, 1);
  CGContextFillRect (myContext, CGRectMake(0, 0, 1, 1));

  CGContextBeginPath (myContext);
  CGContextAddArc (myContext, .5, .5, .3, 0,
            my_convert_to_radians (180), 0);
  CGContextClosePath (myContext);
  CGContextClip (myContext);

  CGContextDrawShading (myContext, shading);
  CGColorSpaceRelease (colorspace);
  CGShadingRelease (shading);
  CGFunctionRelease (myShadingFunction);

  CGContextRestoreGState (myContext); 
}

使用CGShading对象绘制一个径向渐变

这个例子演示了如何使用CGShading对象来生成如图8-12所示的输出

Figure 8-12 A radial gradient created using a CGShading object

image

为了绘制一个径向渐变,我们需要按以下步骤来处理:

  1. 设置CGFunction对象来计算颜色值
  2. 创建径向渐变的CGShading对象
  3. 使用CGShading对象来绘制径向渐变
  4. 释放对象
设置CGFunction对象来计算颜色值

计算径向渐变和轴向渐变颜色值的函数没有什么区别。事实上,我们可以依照上面的轴向的”设置CGFunction对象来计算颜色值”。代码清单8-12用于计算颜色,使用颜色按正弦变化。图8-12与图8-11的结果非常不同。虽然颜色输出值不同,代码清单8-12的代码与8-6中的函数遵循相同的原型。每个函数获取一个输入值并计算N个值,即颜色空间的每个颜色组件加一个alpha值。

Listing 8-12 Computing color component values

static void  myCalculateShadingValues (void *info,
                const CGFloat *in,
                CGFloat *out)
{
  size_t k, components;
  double frequency[4] = { 55, 220, 110, 0 };
  components = (size_t)info;
  for (k = 0; k < components - 1; k++)
    *out++ = (1 + sin(*in * frequency[k]))/2;
   *out++ = 1; // alpha
}

在写完颜色计算函数后调用它,我们需要创建一个CGFunction对象,如在轴向中”设置CGFunction对象来计算颜色值”所描述的一样。

创建径向渐变的CGShading对象

为了创建一个CGShading对象或者一个径向渐变,我们调用CGShadingCreateRadial函数,如代码清单8-13所求,传递一个颜色空间、开始点和结束点,开始半径和结束半径,一个CGFunction对象,和一个用于指定是否填充渐变的开始点和结束点扩展的布尔值。

Listing 8-13 Creating a CGShading object for a radial gradient

CGPoint startPoint, endPoint;
CGFloat startRadius, endRadius;

startPoint = CGPointMake(0.25,0.3);
startRadius = .1;
endPoint = CGPointMake(.7,0.7);
endRadius = .25;
colorspace = CGColorSpaceCreateDeviceRGB();
myShadingFunction = myGetFunction (colorspace);
CGShadingCreateRadial (colorspace,
  startPoint,
  startRadius,
  endPoint,
  endRadius,
  myShadingFunction,
  false,
  false);
使用CGShading对象来绘制径向渐变

调用函数CGContextDrawShading使用CGShading对象为指定的颜色渐变来填充当前上下文:

CGContextDrawShading (myContext, myShading);

注意我们使用相同的函数来绘制渐变,而不管它是轴向还是径向。

释放对象

当我们不再需要CGShading对象时,可以调用函数CGShadingRelease来释放它。我们需要同时释放CGColorSpace对象和CGFunction对象,如代码清单8-14所示:

Listing 8-10 Releasing objects

CGShadingRelease (myShading);
CGColorSpaceRelease (colorspace);
CGFunctionRelease (myFunctionObject);
使用CGShading对象绘制径向渐变的完整例程

代码清单8-15显示了绘制一个轴径向渐变的完整例程,使用8-7中的CGFunction对象和8-12中的回调函数。

Listing 8-15 A routine that paints a radial gradient using a CGShading object

void myPaintRadialShading (CGContextRef myContext,
              CGRect bounds);
{
  CGPoint startPoint,
      endPoint;
  CGFloat startRadius,
      endRadius;
  CGAffineTransform myTransform;
  CGFloat width = bounds.size.width;
  CGFloat height = bounds.size.height;

  startPoint = CGPointMake(0.25,0.3); 
  startRadius = .1;  
  endPoint = CGPointMake(.7,0.7); 
  endRadius = .25; 

  colorspace = CGColorSpaceCreateDeviceRGB(); 
  myShadingFunction = myGetFunction (colorspace);

  shading = CGShadingCreateRadial (colorspace, 
              startPoint, startRadius,
              endPoint, endRadius,
              myShadingFunction,
              false, false);

  myTransform = CGAffineTransformMakeScale (width, height); 
  CGContextConcatCTM (myContext, myTransform); 
  CGContextSaveGState (myContext); 

  CGContextClipToRect (myContext, CGRectMake(0, 0, 1, 1)); 
  CGContextSetRGBFillColor (myContext, 1, 1, 1, 1);
  CGContextFillRect (myContext, CGRectMake(0, 0, 1, 1));

  CGContextDrawShading (myContext, shading); 
  CGColorSpaceRelease (colorspace); 
  CGShadingRelease (shading);
  CGFunctionRelease (myShadingFunction);

  CGContextRestoreGState (myContext); 
}
 
 
 

七.Quartz路径


         Core Graphics(Quartz 2D)中有4个基本图元用于描述路径:点、线段、弧和贝赛尔曲线。


        1.点

        一个点完全不占空间,所以画一个点不会再屏幕上显示任何东西,我们可以在路径里加入很多的点,想加多少加多少。

        2.线段

        线段有两个点定义:起点 和 终点。线段可以通过描边绘制出来,我们可以通过设置图形上下文,如画笔宽度或者颜色等参数,就可以绘制出两点之间的线段。线段没有面积,所以他们不能被填充。

        3.弧

        弧可以由一个圆心点、半径、起始角和结束角描述的。园是弧的特例,只需要设置为起始角0°,结束角为360°就可以了。因为弧是占有一定面积的路径,所以可以被填充、描边和描边填充出来。

        4.贝赛尔曲线

        任何一条曲线都可以通过与它相切的控制线两端的点的位置来定义。因此,贝塞尔曲线可以用4个点描述,其中两个点描述两个端点,另外两个描述每一端的切线。贝塞尔曲线可分为:二次方贝塞尔曲线和高阶贝塞尔曲线。


例如:

CGContextRef context = UIGraphicsGetCurrentContext();//获得访问图形上下文对象
CGContextMoveToPoint(context ,75,10);//在<span style="font-family: Arial, Helvetica, sans-serif;">(75,10)绘制起始点</span>
<pre name="code" class="objc">CGContextAddLineToPoint(context ,10,150);//绘制从(75,10)到(10,150)的线段
CGContextAddLineToPoint(context ,160,150);//绘制从(10,150)到(160,150)的线段CGContextClosePath(context );//闭合

 


        定制和绘制路径是两个不同的操作,先定义路径,在绘制它。CGContextDrawPath(context,kCGPathFillStroke)函数实现绘制路径,其中kCGPathFillStroke参数是填充描边处理,此外还有:kCGPathFillStroke和kCGPathStroke,分别代表填充和描边处理。


绘制路径相关函数:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值