Animate a Change in Part of an Nsmutableattributedstring

Animate a change in part of an NSMutableAttributedString

I achieved what I wanted to by getting the frames for the actual parentheses and creating new UILabels on top of my UITextView and animating those labels.

    @IBAction func didPressClosingParentheses(sender: AnyObject) {

inputTextView.text = inputTextView.text + ")"
var count = 1
let currentString = inputTextView.attributedText.string
let characterArray = Array(currentString)
let closingIndex = characterArray.count - 1
for i in reverse(0...closingIndex-1) {
if characterArray[i] == "(" {
count--
}
else if characterArray[i] == ")" {
count++
}
if count == 0 {
let startingIndex = i

let openingRange = NSMakeRange(startingIndex, 1)
let closingRange = NSMakeRange(closingIndex, 1)

var openingFrame = inputTextView.layoutManager.boundingRectForGlyphRange(openingRange, inTextContainer: inputTextView.textContainer)

openingFrame.origin.y += inputTextView.textContainerInset.top
var openingLabel = UILabel(frame: openingFrame)
openingLabel.text = "("
openingLabel.font = UIFont(name: "HelveticaNeue-Thin", size: 28)
openingLabel.textColor = whiteishColor
openingLabel.backgroundColor = bluishColor

var closingFrame = inputTextView.layoutManager.boundingRectForGlyphRange(closingRange, inTextContainer: inputTextView.textContainer)
closingFrame.origin.y += inputTextView.textContainerInset.top
var closingLabel = UILabel(frame: closingFrame)
closingLabel.text = ")"
closingLabel.font = UIFont(name: "HelveticaNeue-Thin", size: 28)
closingLabel.textColor = whiteishColor
closingLabel.backgroundColor = bluishColor

inputTextView.addSubview(openingLabel)
inputTextView.addSubview(closingLabel)

UIView.animateWithDuration(0.4, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.8, options: nil, animations: {
openingLabel.transform = CGAffineTransformMakeScale(1.25, 1.25)
closingLabel.transform = CGAffineTransformMakeScale(1.25, 1.25)

}, nil)

UIView.animateWithDuration(0.4, delay: 0.2, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.8, options: nil, animations: {
openingLabel.transform = CGAffineTransformMakeScale(1.0, 1.0)
closingLabel.transform = CGAffineTransformMakeScale(1.0, 1.0)

}, nil)
UIView.animateWithDuration(0.25, delay: 0.4, options: nil, animations: {
openingLabel.alpha = 0
closingLabel.alpha = 0

}, completion: { finished in
openingLabel.removeFromSuperview()
closingLabel.removeFromSuperview()
})

break
}
}

}

How to animate NSAttributedString?

If you need to animate a property that is not animatable, you can use CADisplayLink. That allows you to create your own animation by executing a method every time the screen refreshes. Although it looks sort of low-level, it is not really difficult to implement it.
You can find tutorials/examples on the web easily.

Note: It does not trigger your method periodically, therefore do not assume anything about timing and keep a counter and calculate progress your animation by yourself. Apart from that, CADisplayLink is cool.

Edit: As Max pointed out, CADisplayLink is not available for Mac. But there is CVDisplayLink, I guess you can achieve the desired animation by that. For more info on CVDisplayLink, you can have a look at that answer by Brad Larson.

Change attributes of multiple parts of a NSMutableAttributedString

Searching for start-end characters or character sequences is ok if you are certain they will never appear in resultX strings...

One approach would be to build a string piece by piece and create an array of ranges on the fly. At the end you simply slap the attributes on them.

NSMutableString *mTextToDraw = [[NSMutableString alloc] init];
NSMutableArray *mRangesToMod = [[NSMutableArray alloc] init];
NSValue *mRange = nil;

[mTextToDraw appendFormat:@"Summary of currently worked on process\n\nTitle name:\nResults for Process\n "];

mRange = [NSValue valueWithRange:NSMakeRange(mTextToDraw.length,resultOne.length)];;
[mRangesToMod addObject: mRange];
[mString appendFormat:@"%@\n",resultOne];

mRange = [NSValue valueWithRange:NSMakeRange(mTextToDraw.length,resultTwo.length)];;
[mRangesToMod addObject: mRange];
[mString appendFormat:@"%@\n",resultTwo];

mRange = [NSValue valueWithRange:NSMakeRange(mTextToDraw.length,resultThree.length)];;
[mRangesToMod addObject: mRange];
[mString appendFormat:@"%@\n",resultThree];

NSMutableAttributedString *summaryBody = [[NSMutableAttributedString alloc] initWithString:mTextToDraw];

for (NSValue *vRange in mRangesToMod)
{
NSRange range = vRange.rangeValue;

[summaryBody setAttributes:@{NSFontAttributeName:[UIFont fontWithName:@"Roboto-Light" size:smallFont]}
range:range];

// or you can use addAttribute...
}

I used "Roboto-Light" in my app - it won't work in yours as is. Just as an example. For test just put there some font other then the basic (original) one...

Is is possible to animate text color changing only in a part of text in iOS?

You can quite easily achieve this using CoreAnimation possibilities.
I've added a simple demo, you play with it here (just build the project and tap anywhere to see the animation).

The logic is the following:

  1. Create a custom subclass of UIView.
  2. When some text is set, create two similar CATextLayers, each with the same text and frame.
  3. Set different foregroundColor and mask for those layers. The mask of the left layer will be the left part of the view, and the mask of the right layer will be the right part.
  4. Animate foregroundColor for those layers (simultaneously).

The code of a custom view:

class CustomTextLabel: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .green
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private var textLayer1: CATextLayer?
private var textLayer2: CATextLayer?

func setText(_ text: String, fontSize: CGFloat) {
// create 2 layers with the same text and size, we'll set the colors for them later
textLayer1 = createTextLayer(text, fontSize: fontSize)
textLayer2 = createTextLayer(text, fontSize: fontSize)

// estimate the frame size needed for the text layer with such text and font size
let textSize = textLayer1!.preferredFrameSize()
let w = frame.width, h = frame.height

// calculate the frame such that both layers will be in center of view
let centeredTextFrame = CGRect(x: (w-textSize.width)/2, y: (h-textSize.height)/2, width: textSize.width, height: textSize.height)
textLayer1!.frame = centeredTextFrame
textLayer2!.frame = centeredTextFrame

// set up default color for the text
textLayer1!.foregroundColor = UIColor.yellow.cgColor
textLayer2!.foregroundColor = UIColor.yellow.cgColor

// set background transparent, that's very important
textLayer1!.backgroundColor = UIColor.clear.cgColor
textLayer2!.backgroundColor = UIColor.clear.cgColor

// set up masks, such that each layer's text is visible only in its part
textLayer1!.mask = createMaskLayer(CGRect(x: 0, y: 0, width: textSize.width/2, height: textSize.height))
textLayer2!.mask = createMaskLayer(CGRect(x: textSize.width/2, y: 0, width: textSize.width/2, height: textSize.height))

layer.addSublayer(textLayer1!)
layer.addSublayer(textLayer2!)
}

private var finishColor1: UIColor = .black, finishColor2: UIColor = .black
func animateText(leftPartColor1: UIColor, leftPartColor2: UIColor, rightPartColor1: UIColor, rightPartColor2: UIColor) {
finishColor1 = leftPartColor2
finishColor2 = rightPartColor2

if let layer1 = textLayer1, let layer2 = textLayer2 {
CATransaction.begin()
let animation1 = CABasicAnimation(keyPath: "foregroundColor")
animation1.fromValue = leftPartColor1.cgColor
animation1.toValue = leftPartColor2.cgColor
animation1.duration = 3.0
layer1.add(animation1, forKey: "animation1")

let animation2 = CABasicAnimation(keyPath: "foregroundColor")
animation2.fromValue = rightPartColor1.cgColor
animation2.toValue = rightPartColor2.cgColor
animation2.duration = 3.0
layer2.add(animation2, forKey: "animation2")

CATransaction.setCompletionBlock {
self.textLayer1?.foregroundColor = self.finishColor1.cgColor
self.textLayer2?.foregroundColor = self.finishColor2.cgColor
}

CATransaction.commit()
}
}

private func createTextLayer(_ text: String, fontSize: CGFloat) -> CATextLayer {
let textLayer = CATextLayer()
textLayer.string = text
textLayer.fontSize = fontSize // TODO: also set font name
textLayer.contentsScale = UIScreen.main.scale

return textLayer
}

private func createMaskLayer(_ holeRect: CGRect) -> CAShapeLayer {
let layer = CAShapeLayer()

let path = CGMutablePath()

path.addRect(holeRect)
path.addRect(bounds)

layer.path = path
layer.fillRule = CAShapeLayerFillRule.evenOdd
layer.opacity = 1

return layer
}
}

The calls of a custom view:

class ViewController: UIViewController {

var customLabel: CustomTextLabel!
override func viewDidLoad() {
super.viewDidLoad()

let viewW = view.frame.width, viewH = view.frame.height
let labelW: CGFloat = 200, labelH: CGFloat = 50

customLabel = CustomTextLabel(frame: CGRect(x: (viewW-labelW)/2, y: (viewH-labelH)/2, width: labelW, height: labelH))
customLabel.setText("Optimizing...", fontSize: 20)
view.addSubview(customLabel)

let tapRecogniner = UITapGestureRecognizer(target: self, action: #selector(onTap))
view.addGestureRecognizer(tapRecogniner)
}

@objc func onTap() {
customLabel.animateText(leftPartColor1: UIColor.blue,
leftPartColor2: UIColor.red,
rightPartColor1: UIColor.white,
rightPartColor2: UIColor.black)
}

}


Related Topics



Leave a reply



Submit