Gradient Color
var gradientLayer: CAGradientLayer!
The gradientLayer property is going to be our test object from now on in this post. The minimum implementation we have to do in order to display a gradient layer in a target view can be outlined in the following simple steps:
Initialise the CAGradientLayer object (the gradientLayer in our case).
Set the frame of the gradient layer.
Set the colours you want to be used for producing the gradient effect.
Add the gradient layer as a sublayer to the target view’s layer.
On top of those steps, there are more properties that can, and actually should be configured as well. We’ll see them in the following parts. For now, let’s focus on the above steps only. In order to keep things simple, we’ll use the default view of the ViewController class as the target view for our experiments, and we’ll fill it with gradient colours.
In the ViewController class now, let’s create a new method which we’ll use for initialising and setting some default values to the gradientLayer property:
func createGradientLayer() {
gradientLayer = CAGradientLayer()
gradientLayer.frame = self.view.bounds
gradientLayer.colors = [UIColor.redColor().CGColor, UIColor.yellowColor().CGColor]
self.view.layer.addSublayer(gradientLayer)
}
Here’s what is taking place in the above snippet (in fast forward mode): At first, we initialise the gradientLayer object. Then we set its frame and we make it equal to the view controller view’s bounds. Next we specify the colors we want for the gradient effect, and lastly we add the gradient layer as a sublayer to the default layer of the view.
If we call the above in the viewWillAppear(_: ) method:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
createGradientLayer()
}
No bad at all, given the fact that we wrote four simple lines of code only. Let’s dive to some details now.
Gradient Colors
Even though the previous code snippet is really simple, there is actually one line that contains an important property: The colours property. Firstly, I think it’s needless to say that if you don’t use it to set colours, you’ll see no gradient at all. Secondly, this property expects an array of colours (to be honest, it expects an array of AnyObject objects) but not UIColor objects; instead the colours must always be CGColor objects. In the above example I used two colours only, but you can have as many colours as you want. For example, if we use the following set of colours:
gradientLayer.colors = [UIColor.redColor().CGColor, UIColor.orangeColor().CGColor, UIColor.blueColor().CGColor, UIColor.magentaColor().CGColor, UIColor.yellowColor().CGColor]
One great thing with the colours property is that it’s animatable, meaning that you can change the gradient effect’s colours in an animated way. To demonstrate that, let’s create a collection of colour arrays that we can use. We’ll make each colour set (the colours from each array) to be applied when we tap once to the view, but the transition from one to another colour set will take place with an animation.
First things first, so go to the top of the ViewController class and declare the following two new properties right after the gradientLayer property:
var colorSets = [[CGColor]]()
var currentColorSet: Int!
The colorSets array expects as elements other arrays with CGColor objects. The currentColorSet will indicate the color set that is currently being applied to the gradient effect.
Now, let’s create the colour sets. The following colours are just a sample, you can use any colours you may wish:
func createColorSets() {
colorSets.append([UIColor.redColor().CGColor, UIColor.yellowColor().CGColor])
colorSets.append([UIColor.greenColor().CGColor, UIColor.magentaColor().CGColor])
colorSets.append([UIColor.grayColor().CGColor, UIColor.lightGrayColor().CGColor])
currentColorSet = 0
}
In the above method, except for just preparing the arrays with the desired colours and appending them to the colorSets array, we also set the initial value of the currentColorSet property.
Let’s call it now in the viewDidLoad() method so the app executes that piece of code:
override func viewDidLoad() {
super.viewDidLoad()
createColorSets()
}
We also need to make a small change in the createGradientLayer() method. Find the following line:
gradientLayer.colors = [UIColor.redColor().CGColor, UIColor.orangeColor().CGColor, UIColor.blueColor().CGColor, UIColor.magentaColor().CGColor, UIColor.yellowColor().CGColor]
gradientLayer.colors = colorSets[currentColorSet]
Now the colours specified in the array indicated by the currentColorSet property will be used instead of the default colours we had before that change.
I said before that we’ll trigger the animated transition between colours simply by tapping on the view. That means that we need to add a tap gesture recogniser to the view, so while being in the viewDidLoad() let’s do so:
override func viewDidLoad()
{
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.handleTapGesture(_:)))
self.view.addGestureRecognizer(tapGestureRecognizer)
}
The handleTapGesture(_:) method will be called when the view receives the tap gesture. It doesn’t exist so far, so let’s create it. Notice that a CABasicAnimation is used in the following code so it’s possible to animate the layer’s colours property. At that point I take it for granted that you have some basic knowledge about the CABasicAnimation class and how animations are achieved with it. If not, then a quick search on the web will return interesting results. In any case, I’m excluding some properties and I’m just keeping the basics needed to perform the animation.
func handleTapGesture(gestureRecognizer: UITapGestureRecognizer) {
if currentColorSet < colorSets.count - 1 {
currentColorSet! += 1
} else {
currentColorSet = 0
}
let colorChangeAnimation = CABasicAnimation(keyPath: "colors")
colorChangeAnimation.duration = 2.0
colorChangeAnimation.toValue = colorSets[currentColorSet]
colorChangeAnimation.fillMode = kCAFillModeForwards
colorChangeAnimation.removedOnCompletion = false
gradientLayer.addAnimation(colorChangeAnimation, forKey: "colorChange")
}
Initially we determine what the index of the next colour set should be. If the currently selected colour set is the last one in the colorSets array, then we start again from the beginning (currentColorSet = 0) otherwise we just increase the currentColorSet property by one.
The next bunch of code regards the animation. The most important properties specified here are the duration that means how long the animation will last, and the toValue that sets the desired final value we want for the property colours(which is specified upon the CABasicAnimation class initialisation). The other two properties make the animated changes stay to the layer, and not revert back to the original colours. However, that’s not permanent. We need to explicitly set the new gradient colours. When? Right after the animation has finished. This can be achieved by overriding the following method that’s being called when the CABasicAnimation gets finished:
override func animationDidStop(anim: CAAnimation, finished flag: Bool)
{
if flag {
gradientLayer.colors = colorSets[currentColorSet]
}
}
And one more addition to the handleTapGesture(_:) method, so the above method has an actual effect:
func handleTapGesture(gestureRecognizer: UITapGestureRecognizer) {
// Add this line to make the ViewController class the delegate of the animation object.
colorChangeAnimation.delegate = self
gradientLayer.addAnimation(colorChangeAnimation, forKey: "colorChange")
}
That’s all. Feel free to increase or decrease the animation duration. I intentionally set two seconds duration, so it’s easy to see the gradient colours changing:
Color Locations
Knowing how to set or change the colours of the gradient effect consists of the basics regarding the gradient layers, but that’s not enough if you want to have full control over the final result. It’s equally useful to know how to modify the area covered by each colour on the layer and override the default layout of the colours.
If you look at any gradient effect created previously, you’ll notice that each colour is occupying the half of the layer by default.
That can change by using a property called locations provided by the CAGradientLayer class. That property expects an array with NSNumber objects as a value, where each number determines the starting location for each colour. Also, and that’s important, those numbers should mandatorily range from 0.0 to 1.0.
To make it perfectly clear, all you need is to see it action. Go to the createGradientLayer() method, and add that line:
gradientLayer.locations = [0.0, 0.35]
Here’s how the gradient is affected when the app runs again.
The second colours starting location is according to the second value we set in the locations array. We can also say that the second colours covers the 65% of the layer (1.0 – 0.35 = 0.65). As a precaution, make sure that the location of an earlier colour is not greater than the location of a colour that comes next, otherwise an undesired overlapping will take place.
If you want to see the above happening in your own app, just set the [0.5, 0.35] values as the locations for the colours.
Let’s make the demo app now a bit more spicy, and let’s add a new tap gesture recogniser to the view. This time we require two fingers for the tap. In the viewDidLoad() add the next lines:
override func viewDidLoad() {
let twoFingerTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.handleTwoFingerTapGesture(_:)))
twoFingerTapGestureRecognizer.numberOfTouchesRequired = 2
self.view.addGestureRecognizer(twoFingerTapGestureRecognizer)
}
In the handleTwoFingerTapGesture(_:) method we’ll create random values for the locations of both colours. However, we’ll make sure that the first location value is always smaller than the second one’s. In addition to that, the new locations will be printed to the console at the same time. Here it is:
func handleTwoFingerTapGesture(gestureRecognizer: UITapGestureRecognizer) {
let secondColorLocation = arc4random_uniform(100)
let firstColorLocation = arc4random_uniform(secondColorLocation - 1)
gradientLayer.locations = [NSNumber(double: Double(firstColorLocation)/100.0), NSNumber(double: Double(secondColorLocation)/100.0)]
print(gradientLayer.locations!)
}
Note that by default the locations property is nil, so watch out for potential crashes when you use it in your code. Besides that, even though we’re working with two colours only in this demo app to keep things simple, every aspect that’s discussed applies when having more than two colours for the gradient effect as well.
Gradient Direction
By knowing at this point how to handle the colour s property of the gradient layer, let’s proceed in learning how we can deal with the direction of the gradient effect.
You can instantly realise that the gradient effect starts from the top and heads towards bottom. Actually, this is the default direction for the colour s applied to any gradient layer, and like everything else, it can be overridden so the final effect gets a different direction.
The CAGradientLayer class provides two properties that can be used in order to specify the direction of the gradient. These are:
- startPoint
- endPoint
A CGPoint value shall be always assigned to any of the above properties, and both the x and y values should range from 0.0 to 1.0. Actually, the startPoint describes the starting coordinates of the first colour, and the endPoint describes the ending coordinates of the last colour, creating that way the direction of the gradient. However, there is an important detail here; the coordinates are expressed in the operating system's coordinate space.
What does this mean?
In iOS, the zero point (starting point) is the top-left corner of the screen (x = 0.0, y = 0.0), while the bottom-right corner is the ending point (x = 1.0, y = 1.0). Any other point is within those coordinates, and as I’ve already said both x and you must be limited in the range 0.0 to 1.0.
The above coordinates are not the same for other operating systems. Take for example a simple window (here’s the Textedit) on Mac:
The starting point here is the bottom-left corner, and the ending point is the top-right corner, and therefore the coordinate space is different than that in iOS.
By default, the startPoint equals to the (0.5, 0.0) point, while the endPoint equals to the (0.5, 1.0). Notice that the x value remains the same, but the y value starts from 0.0 (top) and ends to 1.0 (bottom), so the direction towards bottom is created. If you want to see a different direction fast, go to the createGradientLayer() method and the following two lines:
func createGradientLayer() {
gradientLayer.startPoint = CGPointMake(0.0, 0.5)
gradientLayer.endPoint = CGPointMake(1.0, 0.5)
}
In that case the x value changes from 0.0 to 1.0, while the y value remains unmodified. The above will result in a gradient effect with direction towards right.
To fully understand the gradient direction, modify the above properties by setting any value between 0.0 and 1.0 for both x and y and watch what happens with every new change. Here, we are going to make our demo app more interesting by adding a new functionality so we can play with the gradient direction: We will add a pan gesture to the view, and depending on the gesture movement we’ll change the direction accordingly. We’ll support the following directions:
- Towards Top
- Towards Bottom
- Towards Right
- Towards Left
- From Top-Left to Bottom-Right
- From Top-Right to Bottom-Left
- From Bottom-Left to Top-Right
- From Bottom-Right to Top-Left
enum PanDirections: Int
{
case Right
case Left
case Bottom
case Top
case TopLeftToBottomRight
case TopRightToBottomLeft
case BottomLeftToTopRight
case BottomRightToTopLeft
}
And after that, declare a new property in the ViewController class to indicate the gradient direction:
var panDirection: PanDirections!
The panDirection property will get the proper value depending on the movement of the finger. We are going to handle that as a two-step task: Initially we’ll determine the desired direction and we’ll assign it to the above property. Next, and right after the pan gesture is finished, we’ll set the proper values to the startPoint and endPoint properties, taking into account of course the indicated direction.
Before doing all that, we need to create a new pan gesture recogniser object and add it to the view. For that, go to the viewDidLoad() method and add the lines you see next:
override func viewDidLoad() {
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePanGestureRecognizer(_:)))
self.view.addGestureRecognizer(panGestureRecognizer)
}
In the implementation of the handlePanGestureRecognizer(_:) we will use the velocity property of the gesture recogniser. If that velocity is more than 300.0 points towards any direction (x or y), that direction will be taken into account. The logic is simple to understand: The primary case is to check the velocity on the horizontal axis. The velocity on the vertical axis is a check being made in a secondary level. See the comments to get it:
func handlePanGestureRecognizer(gestureRecognizer: UIPanGestureRecognizer) {
let velocity = gestureRecognizer.velocityInView(self.view)
if gestureRecognizer.state == UIGestureRecognizerState.Changed {
if velocity.x > 300.0 {
// In this case the direction is generally towards Right.
// Below are specific cases regarding the vertical movement of the gesture.
if velocity.y > 300.0 {
// Movement from Top-Left to Bottom-Right.
panDirection = PanDirections.TopLeftToBottomRight
}
else if velocity.y < -300.0 {
// Movement from Bottom-Left to Top-Right.
panDirection = PanDirections.BottomLeftToTopRight
} else {
// Movement towards Right.
panDirection = PanDirections.Right
}
}
else if velocity.x < -300.0 {
// In this case the direction is generally towards Left.
// Below are specific cases regarding the vertical movement of the gesture.
if velocity.y > 300.0 {
// Movement from Top-Right to Bottom-Left.
panDirection = PanDirections.TopRightToBottomLeft
}
else if velocity.y < -300.0 {
// Movement from Bottom-Right to Top-Left.
panDirection = PanDirections.BottomRightToTopLeft
} else {
// Movement towards Left.
panDirection = PanDirections.Left
}
} else {
// In this case the movement is mostly vertical (towards bottom or top).
if velocity.y > 300.0 {
// Movement towards Bottom.
panDirection = PanDirections.Bottom
}
else if velocity.y < -300.0 {
// Movement towards Top.
panDirection = PanDirections.Top
} else {
// Do nothing.
panDirection = nil
}
}
}
else if gestureRecognizer.state == UIGestureRecognizerState.Ended {
changeGradientDirection()
}
}
There are two things to notice above (further than how we determine the pan gesture direction of course):
The panDirection becomes nil if none of the other conditions is satisfied.
The direction is specified in the Changed state of the gesture. When the gesture is ended, a call to the changeGradientDirection() method is taking place, so the new direction to apply based on the value of the panDirection property.
The following method is simple, as we just set the appropriate values to the startPoint and endPoint properties of the gradient layer. Watch how the x and y values are set depending on the gesture direction:
func changeGradientDirection() {
if panDirection != nil {
switch panDirection.rawValue {
case PanDirections.Right.rawValue:
gradientLayer.startPoint = CGPointMake(0.0, 0.5)
gradientLayer.endPoint = CGPointMake(1.0, 0.5)
case PanDirections.Left.rawValue:
gradientLayer.startPoint = CGPointMake(1.0, 0.5)
gradientLayer.endPoint = CGPointMake(0.0, 0.5)
case PanDirections.Bottom.rawValue:
gradientLayer.startPoint = CGPointMake(0.5, 0.0)
gradientLayer.endPoint = CGPointMake(0.5, 1.0)
case PanDirections.Top.rawValue:
gradientLayer.startPoint = CGPointMake(0.5, 1.0)
gradientLayer.endPoint = CGPointMake(0.5, 0.0)
case PanDirections.TopLeftToBottomRight.rawValue:
gradientLayer.startPoint = CGPointMake(0.0, 0.0)
gradientLayer.endPoint = CGPointMake(1.0, 1.0)
case PanDirections.TopRightToBottomLeft.rawValue:
gradientLayer.startPoint = CGPointMake(1.0, 0.0)
gradientLayer.endPoint = CGPointMake(0.0, 1.0)
case PanDirections.BottomLeftToTopRight.rawValue:
gradientLayer.startPoint = CGPointMake(0.0, 1.0)
gradientLayer.endPoint = CGPointMake(1.0, 0.0)
default:
gradientLayer.startPoint = CGPointMake(1.0, 1.0)
gradientLayer.endPoint = CGPointMake(0.0, 0.0)
}
}
}
In case the panDirection is nil, no action is taken at all.
Now run the app and move your finger towards any supported direction. The gradient will change direction too in accordance to the movement you’re making.
For the sample project, you can check it out at GitHub.
Comments
Post a Comment
Thank You.