How can I create a gradient effect using color divisions? - ios

I'm trying to create a table view (custom class not UITableView) where its results have a gradient effect like in the example image below:
What I tried:
I successfully managed to add the correct gradient as a background to every table cell but I need it to be the color of the text of each label not the background of each cell. Question. (I asked this one.)
FAILED ATEMPT:
Creating a custom gradient image and adding it as colorWithPatternImage: to each label but since the gradient is one every cell looks the same. Question.
FAILED ATEMPT:
Last thing to try:
Suppose you have two colors, color1 and color2. A gradient can result by mixing these colors. In the picture above color1 = purple and color2 = orange. It would be easy to create a gradient effect by dividing the gradient in sections based on the number of results and then find the average color of each section and use it as the text color of each corresponding result.
For example:
5 results = 5 divisions.
division1 = purple
division2 = less purple, more orange
division3 = equal purple, equal orange
division4 = least purple, most orange
division5 = orange
The result is not as detailed because each text is a solid color but it is equally impressive when the text is small:
The problem is, for two colors like these:
Purple: 128.0, 0.0, 255.0
Orange: 255.0, 128.0, 0.0
How do you divide it in 5 sections and find the average of each section?
I could do this using the eyedropper tool in pixelmator but only if I knew the fixed number of results, won't work with 6 results.
I can't approach it with math, I don't know where to begin.
Any ideas?

You can use math on the rgb values of the colors.
To get the rgb values, you can use the getRed:green:blue:alpha method on UIColor. Then all you have to do is average the colors together based on how many sections you need.
Here is a function that should return an array of colors based on a start and end color, and how many divisions you need.
Solution
func divideColors(firstColor: UIColor, secondColor: UIColor, sections: Int) -> [UIColor] {
// get rgb values from the colors
var firstRed: CGFloat = 0; var firstGreen: CGFloat = 0; var firstBlue: CGFloat = 0; var firstAlpha: CGFloat = 0;
firstColor.getRed(&firstRed, green: &firstGreen, blue: &firstBlue, alpha: &firstAlpha)
var secondRed: CGFloat = 0; var secondGreen: CGFloat = 0; var secondBlue: CGFloat = 0; var secondAlpha: CGFloat = 0;
secondColor.getRed(&secondRed, green: &secondGreen, blue: &secondBlue, alpha: &secondAlpha)
// function to mix the colors
func mix(_ first: CGFloat, _ second: CGFloat, ratio: CGFloat) -> CGFloat { return first + ratio * (second - first) }
// variable setup
var colors = [UIColor]()
let ratioPerSection = 1.0 / CGFloat(sections)
// mix the colors for each section
for section in 0 ..< sections {
let newRed = mix(firstRed, secondRed, ratio: ratioPerSection * CGFloat(section))
let newGreen = mix(firstGreen, secondGreen, ratio: ratioPerSection * CGFloat(section))
let newBlue = mix(firstBlue, secondBlue, ratio: ratioPerSection * CGFloat(section))
let newAlpha = mix(firstAlpha, secondAlpha, ratio: ratioPerSection * CGFloat(section))
let newColor = UIColor(red: newRed, green: newGreen, blue: newBlue, alpha: newAlpha)
colors.append(newColor)
}
return colors
}
Your question is tagged as Objective-C, but it should be easy enough to convert the Swift code above to Objective-C since you would use the same UIColor API.
Here is some code to test the above function (perfect for a Swift playground).
Testing Code
let sections = 5
let view = UIView(frame: CGRect(x: 0, y: 0, width: 250, height: 50))
for (index, color) in divideColors(firstColor: UIColor.purple, secondColor: UIColor.orange, sections: sections).enumerated() {
let v = UIView(frame: CGRect(x: CGFloat(index) * 250 / CGFloat(sections), y: 0, width: 250 / CGFloat(sections), height: 50))
v.backgroundColor = color
view.addSubview(v)
}
view.backgroundColor = .white
Test Result
It also works for any number of sections and different colors!

So you want this:
There are several ways you could implement this. Here's a way that's pixel-perfect (it draws the gradient through the labels instead of making each label a solid color).
Make a subclass of UILabel. In your subclass, override drawTextInRect: to draw the gradient.
Let's declare the subclass like this:
IB_DESIGNABLE
#interface GradientLabel: UILabel
#property (nonatomic, weak) IBOutlet UIView *gradientCoverageView;
#property (nonatomic, strong) IBInspectable UIColor *startColor;
#property (nonatomic, strong) IBInspectable UIColor *endColor;
#end
Connect the gradientCoverageView outlet to a view that covers the entire area of the gradient—so, a view that covers all five of the labels. This could be the superview of the labels, or it could just be a hidden view that you have set up to (invisibly) fill the same area.
Set startColor to purple and endColor to orange.
We'll draw the gradient-filled text (in our drawTextInRect: override) in three steps:
Call super to just draw the text normally.
Set the graphics context blend mode kCGBlendModeSourceIn. This blend mode tells Core Graphics to draw only where the context has already been drawn, and to overwrite whatever was drawn there. Thus Core Graphics treats the text drawn by super as a mask.
Draw the gradient with the start point at the top of the coverage view and the end point at the bottom of the coverage view, not at the top and bottom of the current label. Thus the gradient spans the entire coverage view, but is masked to only show up where the text of the current label was drawn in step 1.
Here's the implementation:
#implementation GradientLabel
- (void)drawTextInRect:(CGRect)rect {
[super drawTextInRect:rect];
CGContextRef gc = UIGraphicsGetCurrentContext();
NSArray *colors = #[(__bridge id)self.startColor.CGColor, (__bridge id)self.endColor.CGColor];
CGGradientRef gradient = CGGradientCreateWithColors(CGBitmapContextGetColorSpace(gc), (__bridge CFArrayRef)colors, NULL);
UIView *coverageView = self.gradientCoverageView ?: self;
CGRect coverageRect = [coverageView convertRect:coverageView.bounds toView:self];
CGPoint startPoint = coverageRect.origin;
CGPoint endPoint = { coverageRect.origin.x, CGRectGetMaxY(coverageRect) };
CGContextSaveGState(gc); {
CGContextSetBlendMode(gc, kCGBlendModeSourceIn);
CGContextDrawLinearGradient(gc, gradient, startPoint, endPoint, 0);
} CGContextRestoreGState(gc);
CGGradientRelease(gradient);
}
#end
Here's the layout in my storyboard:
For all five of the GradientLabel instances, I set the startColor to purple and the endColor to orange and I connected the gradientCoverageView outlet to the enclosing stack view.

Well, here is how to do the solid-color mathy bit at the end:
If you have n + 1 colors, for 0 <= m <= n:
color.red[m] = (purple.red * (m/n)) + (orange.red * ((n-m)/n);
color.green[m] = (purple.green * (m/n)) + (orange.green * ((n-m)/n));
color.blue[m] = (purple.blue * (m/n)) + (orange.blue * ((n-m)/n));
Hope this helps.

For Xamarin iOS C#
public static List<UIColor> DividedColors(UIColor firstColor, UIColor secondColor, int sections)
{
nfloat firstRed = 0;
nfloat firstGreen = 0;
nfloat firstBlue = 0;
nfloat firstAlpha = 0;
firstColor.GetRGBA(out firstRed, out firstGreen, out firstBlue, out firstAlpha);
nfloat secondRed = 0;
nfloat secondGreen = 0;
nfloat secondBlue = 0;
nfloat secondAlpha = 0;
secondColor.GetRGBA(out secondRed, out secondGreen, out secondBlue, out secondAlpha);
nfloat Mix(nfloat first, nfloat second, nfloat ratio)
{
return first + ratio * (second - first);
}
List<UIColor> colors = new List<UIColor>();
var ratioPerSection = 1.0f / sections;
for (int i = 0; i < sections; i++)
{
var newRed = Mix(firstRed, secondRed, ratioPerSection * i);
var newGreen = Mix(firstGreen, secondGreen, ratioPerSection * i);
var newBlue = Mix(firstBlue, secondBlue, ratioPerSection * i);
var newAlpha = Mix(firstAlpha, secondAlpha, ratioPerSection * i);
var newColor = new UIColor(newRed, newGreen, newBlue, newAlpha);
colors.Add(newColor);
}
return colors;
}

Related

SCNGeometry / SCNCylinder render edges / border only (Store color, clear content)

I'm trying to figure out how to have an SCNScylinder on sceen with only a border / stroke / edge visible. Everything in my scene is working fine and i was thinking of just applying a clear color to specular.contents
is the use of a SCNNode delegate / Metal code required (i'm not using opengl on my scene)
Any pointers? help is appreciated
The WWDC 2014 presentation showed orbiting cubes that had only wireframes. The technique was to use an image with green edges but transparent interior as the material. From AAPLSlideScenegraphSummary.m:
// A node that will help visualize the position of the stars
_wireframeBoxNode = [SCNNode node];
_wireframeBoxNode.rotation = SCNVector4Make(0, 1, 0, M_PI_4);
_wireframeBoxNode.geometry = [SCNBox boxWithWidth:1 height:1 length:1 chamferRadius:0];
_wireframeBoxNode.geometry.firstMaterial.diffuse.contents = [NSImage imageNamed:#"box_wireframe"];
_wireframeBoxNode.geometry.firstMaterial.lightingModelName = SCNLightingModelConstant; // no lighting
_wireframeBoxNode.geometry.firstMaterial.doubleSided = YES; // double sided
For a similar effect with SCNCylinder, you might need to pass an array of materials, some with border and some without.
Edit
For High Sierra/iOS 11 and higher, the answer by #mnuages is a simpler/better approach.
it requires some effort but you could inspect your cylinder's geometryElements and geometrySources to build a new SCNGeometry with a primitiveType of SCNGeometryPrimitiveTypeLine
Edit
Starting iOS 11 SCNMaterial exposes the fillMode property that can be used to render a geometry as a wireframe.
I appreciate that this is an old post, but I recently had to implement something like this an ARKit project.
Firstly I created a square image in Adobe Illustrator which had green edges, but a transparent centre. This is called 'outlinedFace'.
I then created an SCNNode (Swift 4) as follows:
import UIKit
import ARKit
class CubeNode: SCNNode {
private var faceArray = [SCNMaterial]()
private let FACE_OUTLINE = "outlinedFace"
/// Initialization With Outline Only
///
/// - Parameters:
/// - width: Optional CGFloat (Defaults To 20cm)
/// - height: Optional CGFloat (Defaults To 20cm)
/// - length: Optional CGFloat (Defaults To 20cm)
/// - colours: [UIColor] - [Front, Right , Back , Left, Top , Bottom]
init(width: CGFloat = 0.2, height: CGFloat = 0.2, length: CGFloat = 0.2) {
super.init()
self.geometry = SCNBox(width: width, height: height, length: length, chamferRadius: 0)
for _ in 0 ..< 6{
let face = SCNMaterial()
face.diffuse.contents = UIImage(named: FACE_OUTLINE)
face.isDoubleSided = true
face.lightingModel = .constant
faceArray.append(face)
}
self.geometry?.materials = faceArray
self.rotation = SCNVector4Make(0, 1, 0, .pi / 4)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The key here is making the contents doubled sided.
This works wonderfully for me!

How to remove labels from after drawing with drawrect()

I am pretty new to drawing programmatically. I've managed to draw a line graph, complete with points, lines, auto-scaling axes, and axis labels. In other screens, I can change characteristics of the graph and when I return to the screen, I refresh it with setNeedsDisplay() in the viewWillAppear function of the containing viewController. The lines are redrawn perfectly when I do this.
The new data that is added in other screens may require rescaling the axes. The problem is that when the graph is redrawn, the number labels on the axes are just added to the graph, without removing the old ones, meaning that some labels may be overwritten, while some old ones just remain there next to the new ones.
I think I see why this happens, in that I am creating a label and adding a subview, but not removing it. I guess I figured that since the lines are erased and redrawn, the labels would be, too. How do I cleanly relabel my axes? Is there a better way to do this? My function for creating the labels is listed below. This function is called by drawRect()
func createXAxisLabels(interval: Float, numIntervals: Int) {
let xstart: CGFloat = marginLeft
let yval: CGFloat = marginTop + graphHeight + 10 // 10 pts below the x-axis
var xLabelVals : [Float] = [0]
var xLabelLocs : [CGFloat] = [] // gives the locations for each label
for i in 0...numIntervals {
xLabelLocs.append(xstart + CGFloat(i) * graphWidth/CGFloat(numIntervals))
xLabelVals.append(Float(i) * interval)
}
if interval >= 60.0 {
xUnits = "Minutes"
xUnitDivider = 60
}
for i in 0...numIntervals {
let label = UILabel(frame: CGRectMake(0, 0, 50.0, 16.0))
label.center = CGPoint(x: xLabelLocs[i], y: yval)
label.textAlignment = NSTextAlignment.Center
if interval < 1.0 {
label.text = "\(Float(i) * interval)"
} else {
label.text = "\(i * Int(interval/xUnitDivider))"
}
label.font = UIFont.systemFontOfSize(14)
label.textColor = graphStructureColor
self.addSubview(label)
}
}
drawRect should just draw the rectangle, not change the view hierarchy. It can be called repeatedly. Those other labels are other views and do their own drawing.
You have a couple of options
Don't use labels -- instead just draw the text onto the rect.
-or-
Add/remove the labels in the view controller.
EDIT (from the comments): The OP provided their solution
I just replaced my code inside the for loop with:
let str : NSString = "\(xLabelVals[i])"
let paraAttrib = NSMutableParagraphStyle()
paraAttrib.alignment = NSTextAlignment.Center
let attributes = [
NSFontAttributeName: UIFont.systemFontOfSize(14),
NSForegroundColorAttributeName: UIColor.whiteColor(),
NSParagraphStyleAttributeName: paraAttrib]
let xLoc : CGFloat = CGFloat(xLabelLocs[i] - 25)
let yLoc : CGFloat = yval - 8.0
str.drawInRect(CGRectMake(xLoc, yLoc, 50, 16), withAttributes: attributes)

Calculate opaque color from alpha + background?

I want to "lighten" a certain color by applying an alpha of 0.3 to it. This is shown on a white background.
I would like to determine the opaque color which corresponds to this semi transparent color shown on a white background. The reason is that I use this as view controllers's background color, and when this background is semitransparent the transitions look bad.
I have not tried anything because have no idea about a reasonable way to approach this except maybe taking a snapshot and get the color of it but this seems a bit of an overkill. Can't find any infos, search results are cluttered with "how to make a background semitransparent" etc
Edit: Putting together imataptool's answer parts (and porting to Swift), this is what I came up with:
extension UIColor {
static func opaqueColorByDisplayingTransparentColorOnBackground(transparentColor: UIColor, backgroundColor: UIColor) -> UIColor {
let bgView = UIView(frame: CGRectMake(0, 0, 1, 1))
bgView.backgroundColor = backgroundColor
let overlayView = UIView(frame: CGRectMake(0, 0, 1, 1))
overlayView.backgroundColor = transparentColor
bgView.addSubview(overlayView)
let image = UIView.imageWithView(bgView)
let provider = CGImageGetDataProvider(image.CGImage)
let pixelData = CGDataProviderCopyData(provider)
let data = CFDataGetBytePtr(pixelData)
return UIColor(
red: CGFloat(data[0]) / 255.0,
green: CGFloat(data[1]) / 255.0,
blue: CGFloat(data[2]) / 255.0,
alpha: 1
)
}
}
extension UIView {
// src http://stackoverflow.com/a/32042439/930450
class func imageWithView(view: UIView) -> UIImage {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0)
view.drawViewHierarchyInRect(view.bounds, afterScreenUpdates: true)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}
}
Edit 1: This isn't to say this is the best way to achieve your specific goal, but it does solve the problem of getting the RGB of a mix of two colors with a particular opacity. You haven't provided enough information about what exactly you are doing for me to give a more specific answer. However, if this solution does what you need with acceptable performance, excellent, run with it.
Edit 2: Refer to this Q&A for new (ish) methods for rendering UIView as bitmaps as of iOS 7 (so if you are supporting iOS 6 you can ignore this, but that's unlikely). The gist of the article is that you can now use the UIView method -drawViewHierarchyInRect:(CGRect)afterScreenUpdates:(BOOL)
I can't say whether or not there exists a mathematical way to calculate the exact answer, but one way of going about it would be:
Add the two views to a parent view (with the opaque view below the transparent view)
Convert the parent view (and all of its subviews) to a UIImage
Sample the UIImage for the color it is composed of
Doing 1 is trivial. You can do 2 with
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, [[UIScreen mainScreen] scale]);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage* img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
Now img contains an image with the color you want. Now you just need to figure out what color it is composed of. I believe you can do that with the code provided in this answer. I'll copy it here for convenience.
CFDataRef pixelData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
const UInt8* data = CFDataGetBytePtr(pixelData);
int pixelInfo = ((image.size.width * y) + x ) * 4; // The image is png
UInt8 red = data[pixelInfo]; // If you need this info, enable it
UInt8 green = data[(pixelInfo + 1)]; // If you need this info, enable it
UInt8 blue = data[pixelInfo + 2]; // If you need this info, enable itgame
CFRelease(pixelData);
red, green, and blue now contain the RGB values of the color at whatever point you pick in your view. You might as well just go to the middle of it. Also, to improve the performance of this process, you might choose to only image a small subsection of the view in question (maybe just one pixel).
If somebody will be looking for more straightforward solution, this post about combining colors applying specific alpha might be helpful.
TLDR
extension UIColor {
func combining(with color: UIColor, fraction f: CGFloat) -> UIColor {
let source = components()
let target = color.components()
return UIColor(
red: interpolate(from: source.r, to: target.r, fraction: f),
green: interpolate(from: source.g, to: target.g, fraction: f),
blue: interpolate(from: source.b, to: target.b, fraction: f),
alpha: 1
)
}
private typealias Components = (
r: CGFloat,
g: CGFloat,
b: CGFloat,
a: CGFloat
)
private func components() -> Components {
var result: Components = (0, 0, 0, 0)
getRed(&result.r, green: &result.g, blue: &result.b, alpha: &result.a)
return result
}
}
func interpolate(from a: CGFloat, to b: CGFloat, fraction: CGFloat) -> CGFloat {
(1 - fraction) * a + fraction * b
}

Update the rotation of a CALayer

I am trying to update the current rotation (and sometimes the position) of a CALayer.
What I am trying to in a couple of simple steps:
Store a couple of CALayers in an array, so I can reuse them
Set the anchor point of all CALayers to 0,0.
Draw CALayer objects where the object starts at a position on a circle
The layers are rotated by the same angle as the circle at that position
Update the position and rotation of the CALayer to match new values
Here is a piece of code I have:
lineWidth is the width of a line
self.items is an array containing the CALayer objects
func updateLines() {
var space = 2 * M_PI * Double(circleRadius);
var spaceAvailable = space / (lineWidth)
var visibleItems = [Int]();
var startIndex = items.count - Int(spaceAvailable);
if (startIndex < 0) {
startIndex = 0;
}
for (var i = startIndex; i < self.items.count; i++) {
visibleItems.append(self.items[i]);
}
var circleCenter = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
/* Each line should move up and rotate accordin to this value */
var anglePerLine: CGFloat = (360 / CGFloat(visibleItems.count)).toRadians()
/* Starting position, 270 degrees is on top */
var startAngle: CGFloat = CGFloat(270).toRadians();
/* Lines default rotation, we rotate it to get the right side up */
var lineAngle: CGFloat = CGFloat(180).toRadians();
for (var itemIndex = 0; itemIndex < visibleItems.count; itemIndex++) {
var itemLayer = self.itemLayers[itemIndex];
itemLayer.opacity = 1 - ((0.9 / visibleItems.count) * itemIndex);
/* Calculate start position of layer */
var x = CGFloat(circleRadius) * cos(startAngle) + CGFloat(circleCenter.x);
var y = CGFloat(circleRadius) * sin(startAngle) + CGFloat(circleCenter.y);
var height = CGFloat((arc4random() % 80) + 10);
/* Set position and frame of layer */
itemLayer.frame = CGRectMake(CGFloat(x), CGFloat(y), CGFloat(lineWidth), height);
itemLayer.position = CGPointMake(CGFloat(x), CGFloat(y));
var currentRotation = CGFloat((itemLayer.valueForKeyPath("transform.rotation.z") as NSNumber).floatValue);
var newRotation = lineAngle - currentRotation;
var rotationTransform = CATransform3DRotate(itemLayer.transform, CGFloat(newRotation), 0, 0, 1);
itemLayer.transform = rotationTransform;
lineAngle += anglePerLine;
startAngle += anglePerLine;
}
}
The result of the first run is exactly as I want it to be:
The second run through this code just doesn't update the CALayers correctly and it starts to look like this:
I think it has to do with my code to update the location and transform properties of the CALayer, but whatever I do, it always results in the last picture.
Answered via Twitter: setting frames and transform is mutually exclusive. Happy to help. Finding my login credentials for SO is harder. :D
Found the answer thanks to #iosengineer on Twitter. When setting a position on the CALayer, you do not want to update the frame of the layer, but you want to update the bounds.
Smooth animation FTW

Animate multiple shapes in UIView

I have a custom class that inherit from UIView. In the draw method I draw several shapes including some circles. I want to animate the color (now stroke color) of the circles independent of each other, e.g. I would like the color of one or more the circles to "pulse" or flash (using ease-in/ease-out and not linearly).
What would be the best way to archive this?
It would be great to be able to use the built-in animation code (CABasicAnimation and the like) but I'm not sure how?
EDIT: Here's the code involved. (I am using Xamarin.iOS but my question is not specific to this).
CGColor[] circleColors;
public override void Draw (RectangleF rect)
{
base.Draw (rect);
using (CGContext g = UIGraphics.GetCurrentContext ()) {
g.SetLineWidth(4);
float size = rect.Width > rect.Height ? rect.Height : rect.Width;
float xCenter = ((rect.Width - size) / 2) + (size/2);
float yCenter = ((rect.Height - size) / 2) + (size/2);
float d = size / (rws.NumCircles*2+2);
var circleRect = new RectangleF (xCenter, yCenter, 0, 0);
for (int i = 0; i < rws.NumCircles; i++) {
circleRect.X -= d;
circleRect.Y -= d;
circleRect.Width += d*2;
circleRect.Height += d*2;
CGPath path = new CGPath ();
path.AddEllipseInRect (circleRect);
g.SetStrokeColor (circleColors [i]);
g.AddPath (path);
g.StrokePath ();
}
}
}
You need to move all your drawing code to a subclass of CALayer, and decide parameters which, once varied, will produce the desired animations. Convert these parameters to the layer's properties, and you can animate the layer's properties with CABasicAnimation (or even [UIView animateXXX]).
See this SO question for more information.
Make sure that you set the layer's rasterizationScale to [UIScreen mainScreen].scale to avoid blurs on Retina.

Resources