Animation rendering

This commit is contained in:
Ali 2022-05-24 23:28:38 +03:00
parent 3d06cbf4a4
commit 591cc53c67
33 changed files with 1368 additions and 461 deletions

View File

@ -212,14 +212,12 @@ using AS::MutexLocker;
displayBlock = ^id{
CHECK_CANCELLED_AND_RETURN_NIL();
ASGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay);
UIImage *image = ASGraphicsCreateImage(self.primitiveTraitCollection, bounds.size, opaque, contentsScaleForDisplay, nil, isCancelledBlock, ^{
for (dispatch_block_t block in displayBlocks) {
CHECK_CANCELLED_AND_RETURN_NIL(ASGraphicsEndImageContext());
if (isCancelledBlock()) return;
block();
}
UIImage *image = ASGraphicsGetImageAndEndCurrentContext();
});
ASDN_DELAY_FOR_DISPLAY();
return image;
@ -228,17 +226,13 @@ using AS::MutexLocker;
displayBlock = ^id{
CHECK_CANCELLED_AND_RETURN_NIL();
if (shouldCreateGraphicsContext) {
ASGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay);
CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); );
}
__block UIImage *image = nil;
void (^workWithContext)() = ^{
CGContextRef currentContext = UIGraphicsGetCurrentContext();
UIImage *image = nil;
if (shouldCreateGraphicsContext && !currentContext) {
//ASDisplayNodeAssert(NO, @"Failed to create a CGContext (size: %@)", NSStringFromCGSize(bounds.size));
return nil;
ASDisplayNodeAssert(NO, @"Failed to create a CGContext (size: %@)", NSStringFromCGSize(bounds.size));
return;
}
// For -display methods, we don't have a context, and thus will not call the _willDisplayNodeContentWithRenderingContext or
@ -252,14 +246,15 @@ using AS::MutexLocker;
}
[self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor];
ASDN_DELAY_FOR_DISPLAY();
};
if (shouldCreateGraphicsContext) {
CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); );
image = ASGraphicsGetImageAndEndCurrentContext();
}
ASDN_DELAY_FOR_DISPLAY();
return ASGraphicsCreateImage(self.primitiveTraitCollection, bounds.size, opaque, contentsScaleForDisplay, nil, isCancelledBlock, workWithContext);
} else {
workWithContext();
return image;
}
};
}
@ -309,9 +304,6 @@ using AS::MutexLocker;
}
__instanceLock__.lock();
ASCornerRoundingType cornerRoundingType = _cornerRoundingType;
CGFloat cornerRadius = _cornerRadius;
CGFloat contentsScale = _contentsScaleForDisplay;
ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext;
__instanceLock__.unlock();
@ -320,48 +312,6 @@ using AS::MutexLocker;
didDisplayNodeContentWithRenderingContext(context, drawParameters);
}
}
if (cornerRoundingType == ASCornerRoundingTypePrecomposited && cornerRadius > 0.0f) {
CGRect bounds = CGRectZero;
if (context == NULL) {
bounds = self.threadSafeBounds;
bounds.size.width *= contentsScale;
bounds.size.height *= contentsScale;
CGFloat white = 0.0f, alpha = 0.0f;
[backgroundColor getWhite:&white alpha:&alpha];
ASGraphicsBeginImageContextWithOptions(bounds.size, (alpha == 1.0f), contentsScale);
[*image drawInRect:bounds];
} else {
bounds = CGContextGetClipBoundingBox(context);
}
ASDisplayNodeAssert(UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self);
UIBezierPath *roundedHole = [UIBezierPath bezierPathWithRect:bounds];
[roundedHole appendPath:[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius * contentsScale]];
roundedHole.usesEvenOddFillRule = YES;
UIBezierPath *roundedPath = nil;
if (borderWidth > 0.0f) { // Don't create roundedPath and stroke if borderWidth is 0.0
CGFloat strokeThickness = borderWidth * contentsScale;
CGFloat strokeInset = ((strokeThickness + 1.0f) / 2.0f) - 1.0f;
roundedPath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(bounds, strokeInset, strokeInset)
cornerRadius:_cornerRadius * contentsScale];
roundedPath.lineWidth = strokeThickness;
[[UIColor colorWithCGColor:borderColor] setStroke];
}
// Punch out the corners by copying the backgroundColor over them.
// This works for everything from clearColor to opaque colors.
[backgroundColor setFill];
[roundedHole fillWithBlendMode:kCGBlendModeCopy alpha:1.0f];
[roundedPath stroke]; // Won't do anything if borderWidth is 0 and roundedPath is nil.
if (*image) {
*image = ASGraphicsGetImageAndEndCurrentContext();
}
}
}
- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously

View File

@ -1539,41 +1539,6 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
}
}
- (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor:(UIColor *)backgroundColor
{
ASPerformBlockOnMainThread(^{
for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) {
// Layers are, in order: Top Left, Top Right, Bottom Right, Bottom Left.
// anchorPoint is Bottom Left at 0,0 and Top Right at 1,1.
BOOL isTop = (idx == 0 || idx == 1);
BOOL isRight = (idx == 1 || idx == 2);
CGSize size = CGSizeMake(radius + 1, radius + 1);
ASGraphicsBeginImageContextWithOptions(size, NO, self.contentsScaleForDisplay);
CGContextRef ctx = UIGraphicsGetCurrentContext();
if (isRight == YES) {
CGContextTranslateCTM(ctx, -radius + 1, 0);
}
if (isTop == YES) {
CGContextTranslateCTM(ctx, 0, -radius + 1);
}
UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, radius * 2, radius * 2) cornerRadius:radius];
[roundedRect setUsesEvenOddFillRule:YES];
[roundedRect appendPath:[UIBezierPath bezierPathWithRect:CGRectMake(-1, -1, radius * 2 + 1, radius * 2 + 1)]];
[backgroundColor setFill];
[roundedRect fill];
// No lock needed, as _clipCornerLayers is only modified on the main thread.
CALayer *clipCornerLayer = _clipCornerLayers[idx];
clipCornerLayer.contents = (id)(ASGraphicsGetImageAndEndCurrentContext().CGImage);
clipCornerLayer.bounds = CGRectMake(0.0, 0.0, size.width, size.height);
clipCornerLayer.anchorPoint = CGPointMake(isRight ? 1.0 : 0.0, isTop ? 1.0 : 0.0);
}
[self _layoutClipCornersIfNeeded];
});
}
- (void)_setClipCornerLayersVisible:(BOOL)visible
{
}
@ -1634,7 +1599,6 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
}
else if (newRoundingType == ASCornerRoundingTypeClipping) {
// Clip corners already exist, but the radius has changed.
[self _updateClipCornerLayerContentsWithRadius:newCornerRadius backgroundColor:self.backgroundColor];
}
}
}

View File

@ -7,161 +7,130 @@
//
#import <AsyncDisplayKit/ASGraphicsContext.h>
#import <AsyncDisplayKit/ASCGImageBuffer.h>
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASConfigurationInternal.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <UIKit/UIGraphics.h>
#import <UIKit/UIImage.h>
#import <objc/runtime.h>
#import <AsyncDisplayKit/ASAvailability.h>
/**
* Our version of the private CGBitmapGetAlignedBytesPerRow function.
*
* In both 32-bit and 64-bit, this function rounds up to nearest multiple of 32
* in iOS 9, 10, and 11. We'll try to catch if this ever changes by asserting that
* the bytes-per-row for a 1x1 context from the system is 32.
*/
static size_t ASGraphicsGetAlignedBytesPerRow(size_t baseValue) {
// Add 31 then zero out low 5 bits.
return (baseValue + 31) & ~0x1F;
#if AS_AT_LEAST_IOS13
#define ASPerformBlockWithTraitCollection(work, traitCollection) \
if (@available(iOS 13.0, tvOS 13.0, *)) { \
UITraitCollection *uiTraitCollection = ASPrimitiveTraitCollectionToUITraitCollection(traitCollection); \
[uiTraitCollection performAsCurrentTraitCollection:^{ \
work(); \
}];\
} else { \
work(); \
}
#else
#define ASPerformBlockWithTraitCollection(work, traitCollection) work();
#endif
/**
* A key used to associate CGContextRef -> NSMutableData, nonatomic retain.
*
* That way the data will be released when the context dies. If they pull an image,
* we will retain the data object (in a CGDataProvider) before releasing the context.
*/
static UInt8 __contextDataAssociationKey;
#pragma mark - Graphics Contexts
void ASGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale)
NS_AVAILABLE_IOS(10)
NS_INLINE void ASConfigureExtendedRange(UIGraphicsImageRendererFormat *format)
{
if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) {
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
return;
if (AS_AVAILABLE_IOS_TVOS(12, 12)) {
// nop. We always use automatic range on iOS >= 12.
} else {
// Currently we never do wide color. One day we could pipe this information through from the ASImageNode if it was worth it.
format.prefersExtendedRange = NO;
}
}
// We use "reference contexts" to get device-specific options that UIKit
// uses.
static dispatch_once_t onceToken;
static CGContextRef refCtxOpaque;
static CGContextRef refCtxTransparent;
dispatch_once(&onceToken, ^{
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 1);
refCtxOpaque = CGContextRetain(UIGraphicsGetCurrentContext());
ASDisplayNodeCAssert(CGBitmapContextGetBytesPerRow(refCtxOpaque) == 32, @"Expected bytes per row to be aligned to 32. Has CGBitmapGetAlignedBytesPerRow implementation changed?");
UIGraphicsEndImageContext();
UIImage *ASGraphicsCreateImageWithOptions(CGSize size, BOOL opaque, CGFloat scale, UIImage *sourceImage,
asdisplaynode_iscancelled_block_t NS_NOESCAPE isCancelled,
void (^NS_NOESCAPE work)())
{
return ASGraphicsCreateImage(ASPrimitiveTraitCollectionMakeDefault(), size, opaque, scale, sourceImage, isCancelled, work);
}
// Make transparent ref context.
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 1);
refCtxTransparent = CGContextRetain(UIGraphicsGetCurrentContext());
UIGraphicsEndImageContext();
UIImage *ASGraphicsCreateImage(ASPrimitiveTraitCollection traitCollection, CGSize size, BOOL opaque, CGFloat scale, UIImage * sourceImage, asdisplaynode_iscancelled_block_t NS_NOESCAPE isCancelled, void (NS_NOESCAPE ^work)()) {
if (@available(iOS 10.0, *)) {
if (true /*ASActivateExperimentalFeature(ASExperimentalDrawingGlobal)*/) {
// If they used default scale, reuse one of two preferred formats.
static UIGraphicsImageRendererFormat *defaultFormat;
static UIGraphicsImageRendererFormat *opaqueFormat;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (AS_AVAILABLE_IOS_TVOS(11, 11)) {
defaultFormat = [UIGraphicsImageRendererFormat preferredFormat];
opaqueFormat = [UIGraphicsImageRendererFormat preferredFormat];
} else {
defaultFormat = [UIGraphicsImageRendererFormat defaultFormat];
opaqueFormat = [UIGraphicsImageRendererFormat defaultFormat];
}
opaqueFormat.opaque = YES;
ASConfigureExtendedRange(defaultFormat);
ASConfigureExtendedRange(opaqueFormat);
});
// These options are taken from UIGraphicsBeginImageContext.
CGContextRef refCtx = opaque ? refCtxOpaque : refCtxTransparent;
CGBitmapInfo bitmapInfo = CGBitmapContextGetBitmapInfo(refCtx);
if (scale == 0) {
scale = ASScreenScale();
UIGraphicsImageRendererFormat *format;
if (sourceImage) {
if (sourceImage.renderingMode == UIImageRenderingModeAlwaysTemplate) {
// Template images will be black and transparent, so if we use
// sourceImage.imageRenderFormat it will assume a grayscale color space.
// This is not good because a template image should be able to tint to any color,
// so we'll just use the default here.
if (AS_AVAILABLE_IOS_TVOS(11, 11)) {
format = [UIGraphicsImageRendererFormat preferredFormat];
} else {
format = [UIGraphicsImageRendererFormat defaultFormat];
}
size_t intWidth = (size_t)ceil(size.width * scale);
size_t intHeight = (size_t)ceil(size.height * scale);
size_t bitsPerComponent = CGBitmapContextGetBitsPerComponent(refCtx);
size_t bytesPerRow = CGBitmapContextGetBitsPerPixel(refCtx) * intWidth / 8;
bytesPerRow = ASGraphicsGetAlignedBytesPerRow(bytesPerRow);
size_t bufferSize = bytesPerRow * intHeight;
CGColorSpaceRef colorspace = CGBitmapContextGetColorSpace(refCtx);
// We create our own buffer, and wrap the context around that. This way we can prevent
// the copy that usually gets made when you form a CGImage from the context.
ASCGImageBuffer *buffer = [[ASCGImageBuffer alloc] initWithLength:bufferSize];
CGContextRef context = CGBitmapContextCreate(buffer.mutableBytes, intWidth, intHeight, bitsPerComponent, bytesPerRow, colorspace, bitmapInfo);
// Transfer ownership of the data to the context. So that if the context
// is destroyed before we create an image from it, the data will be released.
objc_setAssociatedObject((__bridge id)context, &__contextDataAssociationKey, buffer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// Set the CTM to account for iOS orientation & specified scale.
// If only we could use CGContextSetBaseCTM. It doesn't
// seem like there are any consequences for our use case
// but we'll be on the look out. The internet hinted that it
// affects shadowing but I tested and shadowing works.
CGContextTranslateCTM(context, 0, intHeight);
CGContextScaleCTM(context, scale, -scale);
// Save the state so we can restore it and recover our scale in GetImageAndEnd
CGContextSaveGState(context);
// Transfer context ownership to the UIKit stack.
UIGraphicsPushContext(context);
CGContextRelease(context);
} else {
format = sourceImage.imageRendererFormat;
}
// We only want the private bits (color space and bits per component) from the image.
// We have our own ideas about opacity and scale.
format.opaque = opaque;
format.scale = scale;
} else if (scale == 0 || scale == ASScreenScale()) {
format = opaque ? opaqueFormat : defaultFormat;
} else {
if (AS_AVAILABLE_IOS_TVOS(11, 11)) {
format = [UIGraphicsImageRendererFormat preferredFormat];
} else {
format = [UIGraphicsImageRendererFormat defaultFormat];
}
if (opaque) format.opaque = YES;
format.scale = scale;
ASConfigureExtendedRange(format);
}
UIImage * _Nullable ASGraphicsGetImageAndEndCurrentContext() NS_RETURNS_RETAINED
{
if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) {
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// Avoid using the imageWithActions: method because it does not support cancellation at the
// last moment i.e. before actually creating the resulting image.
__block UIImage *image;
NSError *error;
[[[UIGraphicsImageRenderer alloc] initWithSize:size format:format]
runDrawingActions:^(UIGraphicsImageRendererContext *rendererContext) {
ASDisplayNodeCAssert(UIGraphicsGetCurrentContext(), @"Should have a context!");
ASPerformBlockWithTraitCollection(work, traitCollection);
}
completionActions:^(UIGraphicsImageRendererContext *rendererContext) {
if (isCancelled == nil || !isCancelled()) {
image = rendererContext.currentImage;
}
}
error:&error];
if (error) {
NSCAssert(NO, @"Error drawing: %@", error);
}
return image;
}
}
// Bad OS or experiment flag. Use UIGraphics* API.
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
ASPerformBlockWithTraitCollection(work, traitCollection)
UIImage *image = nil;
if (isCancelled == nil || !isCancelled()) {
image = UIGraphicsGetImageFromCurrentImageContext();
}
UIGraphicsEndImageContext();
return image;
}
// Pop the context and make sure we have one.
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL) {
ASDisplayNodeCFailAssert(@"Can't end image context without having begun one.");
return nil;
}
// Read the device-specific ICC-based color space to use for the image.
// For DeviceRGB contexts (e.g. UIGraphics), CGBitmapContextCreateImage
// generates an image in a device-specific color space (for wide color support).
// We replicate that behavior, even though at this time CA does not
// require the image to be in this space. Plain DeviceRGB images seem
// to be treated exactly the same, but better safe than sorry.
static CGColorSpaceRef imageColorSpace;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 0);
UIImage *refImage = UIGraphicsGetImageFromCurrentImageContext();
imageColorSpace = CGColorSpaceRetain(CGImageGetColorSpace(refImage.CGImage));
ASDisplayNodeCAssertNotNil(imageColorSpace, nil);
UIGraphicsEndImageContext();
});
// Retrieve our buffer and create a CGDataProvider from it.
ASCGImageBuffer *buffer = objc_getAssociatedObject((__bridge id)context, &__contextDataAssociationKey);
ASDisplayNodeCAssertNotNil(buffer, nil);
CGDataProviderRef provider = [buffer createDataProviderAndInvalidate];
// Create the CGImage. Options taken from CGBitmapContextCreateImage.
CGImageRef cgImg = CGImageCreate(CGBitmapContextGetWidth(context), CGBitmapContextGetHeight(context), CGBitmapContextGetBitsPerComponent(context), CGBitmapContextGetBitsPerPixel(context), CGBitmapContextGetBytesPerRow(context), imageColorSpace, CGBitmapContextGetBitmapInfo(context), provider, NULL, true, kCGRenderingIntentDefault);
CGDataProviderRelease(provider);
// We saved our GState right after setting the CTM so that we could restore it
// here and get the original scale back.
CGContextRestoreGState(context);
CGFloat scale = CGContextGetCTM(context).a;
// Note: popping from the UIKit stack will probably destroy the context.
context = NULL;
UIGraphicsPopContext();
UIImage *result = [[UIImage alloc] initWithCGImage:cgImg scale:scale orientation:UIImageOrientationUp];
CGImageRelease(cgImg);
return result;
}
void ASGraphicsEndImageContext()
{
if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) {
UIGraphicsEndImageContext();
return;
}
UIGraphicsPopContext();
UIImage *ASGraphicsCreateImageWithTraitCollectionAndOptions(ASPrimitiveTraitCollection traitCollection, CGSize size, BOOL opaque, CGFloat scale, UIImage * sourceImage, void (NS_NOESCAPE ^work)()) {
return ASGraphicsCreateImage(traitCollection, size, opaque, scale, sourceImage, nil, work);
}

View File

@ -6,46 +6,60 @@
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
#import <CoreGraphics/CoreGraphics.h>
@class UIImage;
/**
* Functions for creating one-shot graphics contexts that do not have to copy
* their contents when an image is generated from them. This is efficient
* for our use, since we do not reuse graphics contexts.
*
* The API mirrors the UIGraphics API, with the exception that forming an image
* ends the context as well.
*
* Note: You must not mix-and-match between ASGraphics* and UIGraphics* functions
* within the same drawing operation.
*/
#import <AsyncDisplayKit/ASBlockTypes.h>
#import <AsyncDisplayKit/ASTraitCollection.h>
NS_ASSUME_NONNULL_BEGIN
/**
* Creates a one-shot context.
* A wrapper for the UIKit drawing APIs. If you are in ASExperimentalDrawingGlobal, and you have iOS >= 10, we will create
* a UIGraphicsRenderer with an appropriate format. Otherwise, we will use UIGraphicsBeginImageContext et al.
*
* Behavior is the same as UIGraphicsBeginImageContextWithOptions.
* @param size The size of the context.
* @param opaque Whether the context should be opaque or not.
* @param scale The scale of the context. 0 uses main screen scale.
* @param sourceImage If you are planning to render a UIImage into this context, provide it here and we will use its
* preferred renderer format if we are using UIGraphicsImageRenderer.
* @param isCancelled An optional block for canceling the drawing before forming the image. Only takes effect under
* the legacy code path, as UIGraphicsRenderer does not support cancellation.
* @param work A block, wherein the current UIGraphics context is set based on the arguments.
*
* @return The rendered image. You can also render intermediary images using UIGraphicsGetImageFromCurrentImageContext.
*/
AS_EXTERN void ASGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale);
UIImage *ASGraphicsCreateImageWithOptions(CGSize size, BOOL opaque, CGFloat scale, UIImage * _Nullable sourceImage, asdisplaynode_iscancelled_block_t NS_NOESCAPE _Nullable isCancelled, void (NS_NOESCAPE ^work)(void)) ASDISPLAYNODE_DEPRECATED_MSG("Use ASGraphicsCreateImageWithTraitCollectionAndOptions instead");
/**
* Generates and image and ends the current one-shot context.
* A wrapper for the UIKit drawing APIs. If you are in ASExperimentalDrawingGlobal, and you have iOS >= 10, we will create
* a UIGraphicsRenderer with an appropriate format. Otherwise, we will use UIGraphicsBeginImageContext et al.
*
* Behavior is the same as UIGraphicsGetImageFromCurrentImageContext followed by UIGraphicsEndImageContext.
* @param traitCollection Trait collection. The `work` block will be executed with this trait collection, so it will affect dynamic colors, etc.
* @param size The size of the context.
* @param opaque Whether the context should be opaque or not.
* @param scale The scale of the context. 0 uses main screen scale.
* @param sourceImage If you are planning to render a UIImage into this context, provide it here and we will use its
* preferred renderer format if we are using UIGraphicsImageRenderer.
* @param isCancelled An optional block for canceling the drawing before forming the image.
* @param work A block, wherein the current UIGraphics context is set based on the arguments.
*
* @return The rendered image. You can also render intermediary images using UIGraphicsGetImageFromCurrentImageContext.
*/
AS_EXTERN UIImage * _Nullable ASGraphicsGetImageAndEndCurrentContext(void) NS_RETURNS_RETAINED;
UIImage *ASGraphicsCreateImage(ASPrimitiveTraitCollection traitCollection, CGSize size, BOOL opaque, CGFloat scale, UIImage * _Nullable sourceImage, asdisplaynode_iscancelled_block_t _Nullable NS_NOESCAPE isCancelled, void (NS_NOESCAPE ^work)(void));
/**
* Call this if you want to end the current context without making an image.
* A wrapper for the UIKit drawing APIs.
*
* Behavior is the same as UIGraphicsEndImageContext.
* @param traitCollection Trait collection. The `work` block will be executed with this trait collection, so it will affect dynamic colors, etc.
* @param size The size of the context.
* @param opaque Whether the context should be opaque or not.
* @param scale The scale of the context. 0 uses main screen scale.
* @param sourceImage If you are planning to render a UIImage into this context, provide it here and we will use its
* preferred renderer format if we are using UIGraphicsImageRenderer.
* @param work A block, wherein the current UIGraphics context is set based on the arguments.
*
* @return The rendered image. You can also render intermediary images using UIGraphicsGetImageFromCurrentImageContext.
*/
AS_EXTERN void ASGraphicsEndImageContext(void);
UIImage *ASGraphicsCreateImageWithTraitCollectionAndOptions(ASPrimitiveTraitCollection traitCollection, CGSize size, BOOL opaque, CGFloat scale, UIImage * _Nullable sourceImage, void (NS_NOESCAPE ^work)(void)) ASDISPLAYNODE_DEPRECATED_MSG("Use ASGraphicsCreateImage instead");
NS_ASSUME_NONNULL_END

View File

@ -18,7 +18,9 @@ public func authorizationCurrentOptionText(_ type: SentAuthorizationCodeType, st
let body = MarkdownAttributeSet(font: Font.regular(16.0), textColor: primaryColor)
let bold = MarkdownAttributeSet(font: Font.semibold(16.0), textColor: primaryColor)
return parseMarkdownIntoAttributedString(strings.Login_ShortCallTitle, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil }), textAlignment: .center)
case .call, .flashCall:
case .call:
return NSAttributedString(string: strings.Login_CodeSentCall, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center)
case .flashCall:
return NSAttributedString(string: strings.ChangePhoneNumberCode_Called, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center)
}
}
@ -35,7 +37,13 @@ public func authorizationNextOptionText(currentType: SentAuthorizationCodeType,
let timeString = NSString(format: "%d:%.02d", Int(minutes), Int(seconds))
return (NSAttributedString(string: strings.Login_WillSendSms(timeString as String).string, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false)
}
case .call, .flashCall, .missedCall:
case .call:
if timeout <= 0 {
return (NSAttributedString(string: strings.Login_CodeSentCall, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false)
} else {
return (NSAttributedString(string: String(format: strings.ChangePhoneNumberCode_CallTimer(String(format: "%d:%.2d", minutes, seconds)).string, minutes, seconds), font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false)
}
case .flashCall, .missedCall:
if timeout <= 0 {
return (NSAttributedString(string: strings.ChangePhoneNumberCode_Called, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false)
} else {

View File

@ -606,7 +606,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let wasVisible = self.visibilityStatus
let isVisible: Bool
switch self.visibility {
case let .visible(fraction):
case let .visible(fraction, _):
isVisible = fraction > 0.2
case .none:
isVisible = false

View File

@ -492,7 +492,7 @@ public struct DeviceGraphicsContextSettings {
public class DrawingContext {
public let size: CGSize
public let scale: CGFloat
private let scaledSize: CGSize
public let scaledSize: CGSize
public let bytesPerRow: Int
private let bitmapInfo: CGBitmapInfo
public let length: Int
@ -525,7 +525,7 @@ public class DrawingContext {
f(self.context)
}
public init(size: CGSize, scale: CGFloat = 0.0, opaque: Bool = false, clear: Bool = false) {
public init(size: CGSize, scale: CGFloat = 0.0, opaque: Bool = false, clear: Bool = false, bytesPerRow: Int? = nil) {
assert(!size.width.isZero && !size.height.isZero)
let size: CGSize = CGSize(width: max(1.0, size.width), height: max(1.0, size.height))
@ -539,8 +539,8 @@ public class DrawingContext {
self.scale = actualScale
self.scaledSize = CGSize(width: size.width * actualScale, height: size.height * actualScale)
self.bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(scaledSize.width))
self.length = bytesPerRow * Int(scaledSize.height)
self.bytesPerRow = bytesPerRow ?? DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(scaledSize.width))
self.length = self.bytesPerRow * Int(scaledSize.height)
self.imageBuffer = ASCGImageBuffer(length: UInt(self.length))

View File

@ -3685,7 +3685,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
let itemContentFrame = itemNode.apparentContentFrame
let intersection = itemContentFrame.intersection(visibilityRect)
let fraction = intersection.height / itemContentFrame.height
visibility = .visible(fraction)
let subRect = visibilityRect.intersection(itemFrame).offsetBy(dx: 0.0, dy: -itemFrame.minY)
visibility = .visible(fraction, subRect)
}
var updateVisibility = false
if !onlyPositive {

View File

@ -49,24 +49,7 @@ public struct ListViewItemNodeLayout {
public enum ListViewItemNodeVisibility: Equatable {
case none
case visible(CGFloat)
public static func ==(lhs: ListViewItemNodeVisibility, rhs: ListViewItemNodeVisibility) -> Bool {
switch lhs {
case .none:
if case .none = rhs {
return true
} else {
return false
}
case let .visible(fraction):
if case .visible(fraction) = rhs {
return true
} else {
return false
}
}
}
case visible(CGFloat, CGRect)
}
public struct ListViewItemLayoutParams {

View File

@ -109,6 +109,14 @@ public final class ManagedFile {
}
}
public func position() -> Int64 {
if let queue = self.queue {
assert(queue.isCurrent())
}
return lseek(self.fd, 0, SEEK_CUR);
}
public func sync() {
if let queue = self.queue {
assert(queue.isCurrent())
@ -116,3 +124,39 @@ public final class ManagedFile {
fsync(self.fd)
}
}
public extension ManagedFile {
func write(_ data: Data) -> Int {
if data.isEmpty {
return 0
}
return data.withUnsafeBytes { bytes -> Int in
return self.write(bytes.baseAddress!, count: bytes.count)
}
}
func write(_ value: Int32) {
var value = value
let _ = self.write(&value, count: 4)
}
func write(_ value: UInt32) {
var value = value
let _ = self.write(&value, count: 4)
}
func write(_ value: Int64) {
var value = value
let _ = self.write(&value, count: 8)
}
func write(_ value: UInt64) {
var value = value
let _ = self.write(&value, count: 8)
}
func write(_ value: Float32) {
var value = value
let _ = self.write(&value, count: 4)
}
}

View File

@ -275,6 +275,10 @@ swift_library(
"//submodules/TelegramUI/Components/AudioWaveformComponent:AudioWaveformComponent",
"//submodules/TelegramUI/Components/EditableChatTextNode:EditableChatTextNode",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/EmojiKeyboard:EmojiKeyboard",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC",
"//submodules/Media/LocalAudioTranscription:LocalAudioTranscription",
] + select({

View File

@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AnimationCache",
module_name = "AnimationCache",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/CryptoUtils:CryptoUtils",
"//submodules/ManagedFile:ManagedFile",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,406 @@
import Foundation
import UIKit
import SwiftSignalKit
import CryptoUtils
import ManagedFile
public final class AnimationCacheItemFrame {
public enum Format {
case rgba(width: Int, height: Int, bytesPerRow: Int)
}
public let data: Data
public let range: Range<Int>
public let format: Format
public let duration: Double
public init(data: Data, range: Range<Int>, format: Format, duration: Double) {
self.data = data
self.range = range
self.format = format
self.duration = duration
}
}
public final class AnimationCacheItem {
public let numFrames: Int
private let getFrameImpl: (Int) -> AnimationCacheItemFrame?
public init(numFrames: Int, getFrame: @escaping (Int) -> AnimationCacheItemFrame?) {
self.numFrames = numFrames
self.getFrameImpl = getFrame
}
public func getFrame(index: Int) -> AnimationCacheItemFrame? {
return self.getFrameImpl(index)
}
}
public protocol AnimationCacheItemWriter: AnyObject {
func add(bytes: UnsafeRawPointer, length: Int, width: Int, height: Int, bytesPerRow: Int, duration: Double)
func finish()
}
public protocol AnimationCache: AnyObject {
func get(sourceId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Signal<AnimationCacheItem?, NoError>
}
private func md5Hash(_ string: String) -> String {
let hashData = string.data(using: .utf8)!.withUnsafeBytes { bytes -> Data in
return CryptoMD5(bytes.baseAddress!, Int32(bytes.count))
}
return hashData.withUnsafeBytes { bytes -> String in
let uintBytes = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
return String(format: "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", uintBytes[0], uintBytes[1], uintBytes[2], uintBytes[3], uintBytes[4], uintBytes[5], uintBytes[6], uintBytes[7], uintBytes[8], uintBytes[9], uintBytes[10], uintBytes[11], uintBytes[12], uintBytes[13], uintBytes[14], uintBytes[15])
}
}
private func itemSubpath(hashString: String) -> (directory: String, fileName: String) {
assert(hashString.count == 32)
var directory = ""
for i in 0 ..< 1 {
if !directory.isEmpty {
directory.append("/")
}
directory.append(String(hashString[hashString.index(hashString.startIndex, offsetBy: i * 2) ..< hashString.index(hashString.startIndex, offsetBy: (i + 1) * 2)]))
}
return (directory, hashString)
}
private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
private struct ParameterSet: Equatable {
var width: Int
var height: Int
var bytesPerRow: Int
}
private struct FrameMetadata {
var offset: Int
var length: Int
var duration: Double
}
private let file: ManagedFile
private let completion: (Bool) -> Void
private var currentParameterSet: ParameterSet?
private var contentLengthOffset: Int?
private var isFailed: Bool = false
private var isFinished: Bool = false
private var frames: [FrameMetadata] = []
private var contentLength: Int = 0
private let lock = Lock()
init?(tempPath: String, completion: @escaping (Bool) -> Void) {
guard let file = ManagedFile(queue: nil, path: tempPath, mode: .readwrite) else {
return nil
}
self.file = file
self.completion = completion
}
func add(bytes: UnsafeRawPointer, length: Int, width: Int, height: Int, bytesPerRow: Int, duration: Double) {
self.lock.locked {
if self.isFailed {
return
}
let parameterSet = ParameterSet(width: width, height: height, bytesPerRow: bytesPerRow)
if let currentParameterSet = self.currentParameterSet {
if currentParameterSet != parameterSet {
self.isFailed = true
return
}
} else {
self.currentParameterSet = parameterSet
self.file.write(1 as UInt32)
self.file.write(UInt32(parameterSet.width))
self.file.write(UInt32(parameterSet.height))
self.file.write(UInt32(parameterSet.bytesPerRow))
self.contentLengthOffset = Int(self.file.position())
self.file.write(0 as UInt32)
}
self.frames.append(FrameMetadata(offset: Int(self.file.position()), length: length, duration: duration))
let _ = self.file.write(bytes, count: length)
self.contentLength += length
}
}
func finish() {
var shouldComplete = false
self.lock.locked {
if !self.isFinished {
self.isFinished = true
shouldComplete = true
guard let contentLengthOffset = self.contentLengthOffset else {
self.isFailed = true
return
}
let metadataPosition = self.file.position()
self.file.seek(position: Int64(contentLengthOffset))
self.file.write(UInt32(self.contentLength))
self.file.seek(position: metadataPosition)
self.file.write(UInt32(self.frames.count))
for frame in self.frames {
self.file.write(UInt32(frame.offset))
self.file.write(UInt32(frame.length))
self.file.write(Float32(frame.duration))
}
}
}
if shouldComplete {
self.completion(!self.isFailed)
}
}
}
private final class AnimationCacheItemAccessor {
struct FrameInfo {
let range: Range<Int>
let duration: Double
}
private let data: Data
private let frameMapping: [Int: FrameInfo]
private let format: AnimationCacheItemFrame.Format
init(data: Data, frameMapping: [Int: FrameInfo], format: AnimationCacheItemFrame.Format) {
self.data = data
self.frameMapping = frameMapping
self.format = format
}
func getFrame(index: Int) -> AnimationCacheItemFrame? {
guard let frameInfo = self.frameMapping[index] else {
return nil
}
return AnimationCacheItemFrame(data: data, range: frameInfo.range, format: self.format, duration: frameInfo.duration)
}
}
private func readUInt32(data: Data, offset: Int) -> UInt32 {
var value: UInt32 = 0
withUnsafeMutableBytes(of: &value, { bytes -> Void in
data.withUnsafeBytes { dataBytes -> Void in
memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), 4)
}
})
return value
}
private func loadItem(path: String) -> AnimationCacheItem? {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped) else {
return nil
}
let dataLength = data.count
var offset = 0
guard dataLength >= offset + 4 else {
return nil
}
let formatVersion = readUInt32(data: data, offset: offset)
offset += 4
if formatVersion != 1 {
return nil
}
guard dataLength >= offset + 4 else {
return nil
}
let width = readUInt32(data: data, offset: offset)
offset += 4
guard dataLength >= offset + 4 else {
return nil
}
let height = readUInt32(data: data, offset: offset)
offset += 4
guard dataLength >= offset + 4 else {
return nil
}
let bytesPerRow = readUInt32(data: data, offset: offset)
offset += 4
guard dataLength >= offset + 4 else {
return nil
}
let frameDataLength = readUInt32(data: data, offset: offset)
offset += 4
offset += Int(frameDataLength)
guard dataLength >= offset + 4 else {
return nil
}
let numFrames = readUInt32(data: data, offset: offset)
offset += 4
var frameMapping: [Int: AnimationCacheItemAccessor.FrameInfo] = [:]
for i in 0 ..< Int(numFrames) {
guard dataLength >= offset + 4 + 4 + 4 else {
return nil
}
let frameStart = readUInt32(data: data, offset: offset)
offset += 4
let frameLength = readUInt32(data: data, offset: offset)
offset += 4
let frameDuration = readUInt32(data: data, offset: offset)
offset += 4
frameMapping[i] = AnimationCacheItemAccessor.FrameInfo(range: Int(frameStart) ..< Int(frameStart + frameLength), duration: Double(frameDuration))
}
let itemAccessor = AnimationCacheItemAccessor(data: data, frameMapping: frameMapping, format: .rgba(width: Int(width), height: Int(height), bytesPerRow: Int(bytesPerRow)))
return AnimationCacheItem(numFrames: Int(numFrames), getFrame: { index in
return itemAccessor.getFrame(index: index)
})
}
public final class AnimationCacheImpl: AnimationCache {
private final class Impl {
private final class ItemContext {
let subscribers = Bag<(AnimationCacheItem?) -> Void>()
let disposable = MetaDisposable()
deinit {
self.disposable.dispose()
}
}
private let queue: Queue
private let basePath: String
private let allocateTempFile: () -> String
private var itemContexts: [String: ItemContext] = [:]
init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String) {
self.queue = queue
self.basePath = basePath
self.allocateTempFile = allocateTempFile
}
deinit {
}
func get(sourceId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable {
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId))
let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)"
let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)"
if FileManager.default.fileExists(atPath: itemPath) {
completion(loadItem(path: itemPath))
return EmptyDisposable
}
let itemContext: ItemContext
var beginFetch = false
if let current = self.itemContexts[sourceId] {
itemContext = current
} else {
itemContext = ItemContext()
self.itemContexts[sourceId] = itemContext
beginFetch = true
}
let queue = self.queue
let index = itemContext.subscribers.add(completion)
if beginFetch {
let tempPath = self.allocateTempFile()
guard let writer = AnimationCacheItemWriterImpl(tempPath: tempPath, completion: { [weak self, weak itemContext] success in
queue.async {
guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[sourceId] else {
return
}
strongSelf.itemContexts.removeValue(forKey: sourceId)
guard success else {
return
}
guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else {
return
}
guard let _ = try? FileManager.default.moveItem(atPath: tempPath, toPath: itemPath) else {
return
}
guard let item = loadItem(path: itemPath) else {
return
}
for f in itemContext.subscribers.copyItems() {
f(item)
}
}
}) else {
return EmptyDisposable
}
let fetchDisposable = fetch(writer)
itemContext.disposable.set(ActionDisposable {
fetchDisposable.dispose()
})
}
return ActionDisposable { [weak self, weak itemContext] in
queue.async {
guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[sourceId] else {
return
}
itemContext.subscribers.remove(index)
if itemContext.subscribers.isEmpty {
itemContext.disposable.dispose()
strongSelf.itemContexts.removeValue(forKey: sourceId)
}
}
}
}
}
private let queue: Queue
private let impl: QueueLocalObject<Impl>
public init(basePath: String, allocateTempFile: @escaping () -> String) {
let queue = Queue()
self.queue = queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile)
})
}
public func get(sourceId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Signal<AnimationCacheItem?, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.get(sourceId: sourceId, fetch: fetch, completion: { result in
subscriber.putNext(result)
subscriber.putCompletion()
}))
}
return disposable
}
|> runOn(self.queue)
}
}

View File

@ -0,0 +1,18 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "EmojiKeyboard",
module_name = "EmojiKeyboard",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,4 @@
import Foundation
import UIKit
import Display

View File

@ -0,0 +1,22 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "LottieAnimationCache",
module_name = "LottieAnimationCache",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/Display:Display",
"//submodules/rlottie:RLottieBinding",
"//submodules/GZip:GZip",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,23 @@
import Foundation
import UIKit
import AnimationCache
import Display
import RLottieBinding
import GZip
public func cacheLottieAnimation(data: Data, width: Int, height: Int, writer: AnimationCacheItemWriter) {
let decompressedData = TGGUnzipData(data, 512 * 1024) ?? data
guard let animation = LottieInstance(data: decompressedData, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else {
writer.finish()
return
}
let size = CGSize(width: width, height: height)
let context = DrawingContext(size: size, scale: 1.0, opaque: false, clear: true)
let frameDuration = 1.0 / Double(animation.frameRate)
for i in 0 ..< animation.frameCount {
animation.renderFrame(with: i, into: context.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(context.scaledSize.width), height: Int32(context.scaledSize.height), bytesPerRow: Int32(context.bytesPerRow))
writer.add(bytes: context.bytes, length: context.length, width: Int(context.scaledSize.width), height: Int(context.scaledSize.height), bytesPerRow: Int(context.bytesPerRow), duration: frameDuration)
}
writer.finish()
}

View File

@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MultiAnimationRenderer",
module_name = "MultiAnimationRenderer",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Display:Display",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,421 @@
import Foundation
import UIKit
import SwiftSignalKit
import Display
import AnimationCache
public protocol MultiAnimationRenderer: AnyObject {
func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Disposable
}
open class MultiAnimationRenderTarget: SimpleLayer {
fileprivate let deinitCallbacks = Bag<() -> Void>()
fileprivate let updateStateCallbacks = Bag<() -> Void>()
public final var shouldBeAnimating: Bool = false {
didSet {
if self.shouldBeAnimating != oldValue {
for f in self.updateStateCallbacks.copyItems() {
f()
}
}
}
}
deinit {
for f in self.deinitCallbacks.copyItems() {
f()
}
}
}
private func convertFrameToImage(frame: AnimationCacheItemFrame) -> UIImage? {
switch frame.format {
case let .rgba(width, height, bytesPerRow):
let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow)
let range = frame.range
frame.data.withUnsafeBytes { bytes -> Void in
memcpy(context.bytes, bytes.baseAddress!.advanced(by: range.lowerBound), min(context.length, range.upperBound - range.lowerBound))
}
return context.generateImage()
}
}
private final class FrameGroup {
let image: UIImage
let size: CGSize
let frameRange: Range<Int>
let count: Int
let skip: Int
init?(item: AnimationCacheItem, baseFrameIndex: Int, count: Int, skip: Int) {
if count == 0 {
return nil
}
assert(count % skip == 0)
let actualCount = count / skip
guard let firstFrame = item.getFrame(index: baseFrameIndex % item.numFrames) else {
return nil
}
switch firstFrame.format {
case let .rgba(width, height, bytesPerRow):
let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height * actualCount)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow)
for i in stride(from: baseFrameIndex, to: baseFrameIndex + count, by: skip) {
let frame: AnimationCacheItemFrame
if i == baseFrameIndex {
frame = firstFrame
} else {
if let nextFrame = item.getFrame(index: i % item.numFrames) {
frame = nextFrame
} else {
return nil
}
}
let localFrameIndex = (i - baseFrameIndex) / skip
frame.data.withUnsafeBytes { bytes -> Void in
memcpy(context.bytes.advanced(by: localFrameIndex * height * bytesPerRow), bytes.baseAddress!.advanced(by: frame.range.lowerBound), height * bytesPerRow)
}
}
guard let image = context.generateImage() else {
return nil
}
self.image = image
self.size = CGSize(width: CGFloat(width), height: CGFloat(height))
self.frameRange = baseFrameIndex ..< (baseFrameIndex + count)
self.count = count
self.skip = skip
}
}
func contentsRect(index: Int) -> CGRect? {
if !self.frameRange.contains(index) {
return nil
}
let actualCount = self.count / self.skip
let localIndex = (index - self.frameRange.lowerBound) / self.skip
let itemHeight = 1.0 / CGFloat(actualCount)
return CGRect(origin: CGPoint(x: 0.0, y: CGFloat(localIndex) * itemHeight), size: CGSize(width: 1.0, height: itemHeight))
}
}
private final class LoadFrameGroupTask {
let task: () -> () -> Void
init(task: @escaping () -> () -> Void) {
self.task = task
}
}
private final class ItemAnimationContext {
static let queue = Queue(name: "ItemAnimationContext", qos: .default)
private let cache: AnimationCache
private let stateUpdated: () -> Void
private var disposable: Disposable?
private var displayLink: ConstantDisplayLinkAnimator?
private var frameIndex: Int = 0
private var item: AnimationCacheItem?
private var currentFrameGroup: FrameGroup?
private var isLoadingFrameGroup: Bool = false
private(set) var isPlaying: Bool = false {
didSet {
if self.isPlaying != oldValue {
self.stateUpdated()
}
}
}
let targets = Bag<Weak<MultiAnimationRenderTarget>>()
init(cache: AnimationCache, itemId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable, stateUpdated: @escaping () -> Void) {
self.cache = cache
self.stateUpdated = stateUpdated
self.disposable = cache.get(sourceId: itemId, fetch: fetch).start(next: { [weak self] item in
Queue.mainQueue().async {
guard let strongSelf = self, let item = item else {
return
}
strongSelf.item = item
strongSelf.updateIsPlaying()
}
})
}
deinit {
self.disposable?.dispose()
self.displayLink?.invalidate()
}
func updateIsPlaying() {
var isPlaying = true
if self.item == nil {
isPlaying = false
}
var shouldBeAnimating = false
for target in self.targets.copyItems() {
if let target = target.value {
if target.shouldBeAnimating {
shouldBeAnimating = true
break
}
}
}
if !shouldBeAnimating {
isPlaying = false
}
self.isPlaying = isPlaying
}
func animationTick() -> LoadFrameGroupTask? {
return self.update(advanceFrame: true)
}
private func update(advanceFrame: Bool) -> LoadFrameGroupTask? {
guard let item = self.item else {
return nil
}
let currentFrame = self.frameIndex % item.numFrames
if let currentFrameGroup = self.currentFrameGroup, currentFrameGroup.frameRange.contains(currentFrame) {
} else if !self.isLoadingFrameGroup {
self.currentFrameGroup = nil
self.isLoadingFrameGroup = true
return LoadFrameGroupTask(task: { [weak self] in
let possibleCounts: [Int] = [10, 12, 14, 16, 18, 20]
let countIndex = Int.random(in: 0 ..< possibleCounts.count)
let currentFrameGroup = FrameGroup(item: item, baseFrameIndex: currentFrame, count: possibleCounts[countIndex], skip: 2)
return {
guard let strongSelf = self else {
return
}
strongSelf.isLoadingFrameGroup = false
if let currentFrameGroup = currentFrameGroup {
strongSelf.currentFrameGroup = currentFrameGroup
for target in strongSelf.targets.copyItems() {
target.value?.contents = currentFrameGroup.image.cgImage
}
let _ = strongSelf.update(advanceFrame: false)
}
}
})
}
if advanceFrame {
self.frameIndex += 2
}
if let currentFrameGroup = self.currentFrameGroup, let contentsRect = currentFrameGroup.contentsRect(index: currentFrame) {
for target in self.targets.copyItems() {
target.value?.contentsRect = contentsRect
}
}
return nil
}
}
public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
private final class GroupContext {
private let stateUpdated: () -> Void
private var itemContexts: [String: ItemAnimationContext] = [:]
private(set) var isPlaying: Bool = false {
didSet {
if self.isPlaying != oldValue {
self.stateUpdated()
}
}
}
init(stateUpdated: @escaping () -> Void) {
self.stateUpdated = stateUpdated
}
func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Disposable {
let itemContext: ItemAnimationContext
if let current = self.itemContexts[itemId] {
itemContext = current
} else {
itemContext = ItemAnimationContext(cache: cache, itemId: itemId, fetch: fetch, stateUpdated: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateIsPlaying()
})
self.itemContexts[itemId] = itemContext
}
let index = itemContext.targets.add(Weak(target))
let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in
Queue.mainQueue().async {
guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else {
return
}
itemContext.targets.remove(index)
if itemContext.targets.isEmpty {
strongSelf.itemContexts.removeValue(forKey: itemId)
}
}
}
let updateStateIndex = target.updateStateCallbacks.add { [weak itemContext] in
guard let itemContext = itemContext else {
return
}
itemContext.updateIsPlaying()
}
return ActionDisposable { [weak self, weak itemContext, weak target] in
guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else {
return
}
if let target = target {
target.deinitCallbacks.remove(deinitIndex)
target.updateStateCallbacks.remove(updateStateIndex)
}
itemContext.targets.remove(index)
if itemContext.targets.isEmpty {
strongSelf.itemContexts.removeValue(forKey: itemId)
}
}
}
private func updateIsPlaying() {
var isPlaying = false
for (_, itemContext) in self.itemContexts {
if itemContext.isPlaying {
isPlaying = true
break
}
}
self.isPlaying = isPlaying
}
func animationTick() -> [LoadFrameGroupTask] {
var tasks: [LoadFrameGroupTask] = []
for (_, itemContext) in self.itemContexts {
if itemContext.isPlaying {
if let task = itemContext.animationTick() {
tasks.append(task)
}
}
}
return tasks
}
}
private var groupContexts: [String: GroupContext] = [:]
private var displayLink: ConstantDisplayLinkAnimator?
private(set) var isPlaying: Bool = false {
didSet {
if self.isPlaying != oldValue {
if self.isPlaying {
if self.displayLink == nil {
self.displayLink = ConstantDisplayLinkAnimator { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.animationTick()
}
self.displayLink?.frameInterval = 2
self.displayLink?.isPaused = false
}
} else {
if let displayLink = self.displayLink {
self.displayLink = nil
displayLink.invalidate()
}
}
}
}
}
public init() {
}
public func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Disposable {
let groupContext: GroupContext
if let current = self.groupContexts[groupId] {
groupContext = current
} else {
groupContext = GroupContext(stateUpdated: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateIsPlaying()
})
self.groupContexts[groupId] = groupContext
}
let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, fetch: fetch)
return ActionDisposable {
disposable.dispose()
}
}
private func updateIsPlaying() {
var isPlaying = false
for (_, groupContext) in self.groupContexts {
if groupContext.isPlaying {
isPlaying = true
break
}
}
self.isPlaying = isPlaying
}
private func animationTick() {
var tasks: [LoadFrameGroupTask] = []
for (_, groupContext) in self.groupContexts {
if groupContext.isPlaying {
tasks.append(contentsOf: groupContext.animationTick())
}
}
if !tasks.isEmpty {
ItemAnimationContext.queue.async {
var completions: [() -> Void] = []
for task in tasks {
let complete = task.task()
completions.append(complete)
}
if !completions.isEmpty {
Queue.mainQueue().async {
for completion in completions {
completion()
}
}
}
}
}
}
}

View File

@ -2903,7 +2903,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if !forceOpen {
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
if !found, let itemNode = itemNode as? ChatMessageItemView, itemNode.item?.message.id == message.id, let (action, _, _, _, _) = itemNode.playMediaWithSound() {
if case let .visible(fraction) = itemNode.visibility, fraction > 0.7 {
if case let .visible(fraction, _) = itemNode.visibility, fraction > 0.7 {
action(Double(timestamp))
} else {
let _ = strongSelf.controllerInteraction?.openMessage(message, .timecode(Double(timestamp)))
@ -3564,7 +3564,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}, cancelInteractiveKeyboardGestures: { [weak self] in
(self?.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures()
self?.chatDisplayNode.cancelInteractiveKeyboardGestures()
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(backgroundNode: self.chatBackgroundNode))
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode))
self.controllerInteraction = controllerInteraction
@ -8827,7 +8827,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var hasUnconsumed = false
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView, let (action, _, _, isUnconsumed, _) = itemNode.playMediaWithSound() {
if case let .visible(fraction) = itemNode.visibility, fraction > 0.7 {
if case let .visible(fraction, _) = itemNode.visibility, fraction > 0.7 {
actions.insert((isUnconsumed, action), at: 0)
if !hasUnconsumed && isUnconsumed {
hasUnconsumed = true

View File

@ -332,63 +332,4 @@ public final class ChatControllerInteraction {
self.presentationContext = presentationContext
}
static var `default`: ChatControllerInteraction {
return ChatControllerInteraction(openMessage: { _, _ in
return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in
}, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
}, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in }, navigationController: {
return nil
}, chatControllerNode: {
return nil
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in
}, canSetupReply: { _ in
return .none
}, navigateToFirstDateMessage: { _, _ in
}, requestRedeliveryOfFailedMessages: { _ in
}, addContact: { _ in
}, rateCall: { _, _, _ in
}, requestSelectMessagePollOptions: { _, _ in
}, requestOpenMessagePollResults: { _, _ in
}, openAppStorePage: {
}, displayMessageTooltip: { _, _, _, _ in
}, seekToTimecode: { _, _, _ in
}, scheduleCurrentMessage: {
}, sendScheduledMessagesNow: { _ in
}, editScheduledMessagesTime: { _ in
}, performTextSelectionAction: { _, _, _ in
}, displayImportedMessageTooltip: { _ in
}, displaySwipeToReplyHint: {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in
}, openPollCreation: { _ in
}, displayPollSolution: { _, _ in
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in
}, animateDiceSuccess: { _ in
}, displayPremiumStickerTooltip: { _, _ in
}, openPeerContextMenu: { _, _, _, _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in
}, editMessageMedia: { _, _ in
}, copyText: { _ in
}, displayUndo: { _ in
}, isAnimatingMessage: { _ in
return false
}, getMessageTransitionNode: {
return nil
}, updateChoosingSticker: { _ in
}, commitEmojiInteraction: { _, _, _, _ in
}, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, openWebView: { _, _, _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
pollActionState: ChatInterfacePollActionState(),
stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false),
presentationContext: ChatPresentationContext(backgroundNode: nil)
)
}
}

View File

@ -514,7 +514,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.historyNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
self.textInputPanelNode = ChatTextInputPanelNode(presentationInterfaceState: chatPresentationInterfaceState, presentationContext: ChatPresentationContext(backgroundNode: backgroundNode), presentController: { [weak self] controller in
self.textInputPanelNode = ChatTextInputPanelNode(presentationInterfaceState: chatPresentationInterfaceState, presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode), presentController: { [weak self] controller in
self?.interfaceInteraction?.presentController(controller, nil)
})
self.textInputPanelNode?.storedInputLanguage = chatPresentationInterfaceState.interfaceState.inputLanguage
@ -630,7 +630,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
if let itemNode = itemNode as? ChatMessageItemView, let (_, soundEnabled, isVideoMessage, _, badgeNode) = itemNode.playMediaWithSound(), let node = badgeNode {
if soundEnabled {
skip = true
} else if !skip && !isVideoMessage, case let .visible(fraction) = itemNode.visibility {
} else if !skip && !isVideoMessage, case let .visible(fraction, _) = itemNode.visibility {
nodes.insert((fraction, itemNode, node), at: 0)
}
}

View File

@ -692,17 +692,24 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}
}
var hasRateTranscription = false
if let audioTranscription = audioTranscription {
hasRateTranscription = true
actions.insert(.custom(ChatRateTranscriptionContextItem(context: context, message: message, action: { [weak context] value in
guard let context = context else {
return
}
let _ = context.engine.messages.rateAudioTranscription(messageId: message.id, id: audioTranscription.id, isGood: value).start()
//TODO:localize
let content: UndoOverlayContent = .info(title: nil, text: "Thank you for your feedback.")
controllerInteraction.displayUndo(content)
}), false), at: 0)
actions.insert(.separator, at: 1)
}
if !hasRateTranscription {
for media in message.media {
if let file = media as? TelegramMediaFile, let size = file.size, size < 1 * 1024 * 1024, let duration = file.duration, duration < 60, (["audio/mpeg", "audio/mp3", "audio/mpeg3", "audio/ogg"] as [String]).contains(file.mimeType.lowercased()) {
let fileName = file.fileName ?? "Tone"
@ -739,6 +746,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}
}
}
}
var isReplyThreadHead = false
if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation {
@ -2371,6 +2379,7 @@ private final class ChatRateTranscriptionContextItemNode: ASDisplayNode, Context
self.textNode.isAccessibilityElement = false
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
//TODO:localizable
self.textNode.attributedText = NSAttributedString(string: "Rate Transcription", font: textFont, textColor: presentationData.theme.contextMenu.secondaryColor)
self.textNode.maximumNumberOfLines = 1

View File

@ -24,6 +24,8 @@ import Markdown
import WallpaperBackgroundNode
import ChatPresentationInterfaceState
import ChatMessageBackground
import AnimationCache
import MultiAnimationRenderer
enum InternalBubbleTapAction {
case action(() -> Void)
@ -288,9 +290,37 @@ private enum ContentNodeOperation {
class ChatPresentationContext {
weak var backgroundNode: WallpaperBackgroundNode?
let animationCache: AnimationCache
let animationRenderer: MultiAnimationRenderer
init(backgroundNode: WallpaperBackgroundNode?) {
init(context: AccountContext, backgroundNode: WallpaperBackgroundNode?) {
self.backgroundNode = backgroundNode
self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
self.animationRenderer = MultiAnimationRendererImpl()
}
}
private func mapVisibility(_ visibility: ListViewItemNodeVisibility, boundsSize: CGSize, insets: UIEdgeInsets, to contentNode: ChatMessageBubbleContentNode) -> ListViewItemNodeVisibility {
switch visibility {
case .none:
return .none
case let .visible(fraction, subRect):
var subRect = subRect
subRect.origin.x = 0.0
subRect.size.width = 10000.0
subRect.origin.y = boundsSize.height - insets.top - (subRect.origin.y + subRect.height)
let contentNodeFrame = contentNode.frame
if contentNodeFrame.intersects(subRect) {
let intersectionRect = contentNodeFrame.intersection(subRect)
return .visible(fraction, intersectionRect.offsetBy(dx: 0.0, dy: -contentNodeFrame.minY))
} else {
return .visible(fraction, CGRect())
}
}
}
@ -489,12 +519,22 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
private var currentSwipeAction: ChatControllerInteractionSwipeAction?
//private let debugNode: ASDisplayNode
override var visibility: ListViewItemNodeVisibility {
didSet {
if self.visibility != oldValue {
for contentNode in self.contentNodes {
contentNode.visibility = self.visibility
contentNode.visibility = mapVisibility(self.visibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode)
}
/*switch self.visibility {
case let .visible(_, subRect):
let topEdge = self.bounds.height - self.insets.top - (subRect.origin.y + subRect.height)
self.debugNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topEdge), size: CGSize(width: 100.0, height: 2.0))
case .none:
break
}*/
}
}
}
@ -513,8 +553,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
self.messageAccessibilityArea = AccessibilityAreaNode()
//self.debugNode = ASDisplayNode()
//self.debugNode.backgroundColor = .blue
super.init(layerBacked: false)
//self.addSubnode(self.debugNode)
self.mainContainerNode.shouldBegin = { [weak self] location in
guard let strongSelf = self else {
return false
@ -2650,7 +2695,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
}
containerSupernode.addSubnode(contentNode)
contentNode.visibility = strongSelf.visibility
contentNode.updateIsTextSelectionActive = { [weak contextSourceNode] value in
contextSourceNode?.updateDistractionFreeMode?(value)
}
@ -2729,6 +2773,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} else {
contentNode.frame = contentNodeFrame
}
contentNode.visibility = mapVisibility(strongSelf.visibility, boundsSize: layout.contentSize, insets: strongSelf.insets, to: contentNode)
contentNodeIndex += 1
}

View File

@ -15,6 +15,9 @@ import TelegramAnimatedStickerNode
import SwiftSignalKit
import AccountContext
import YuvConversion
import AnimationCache
import LottieAnimationCache
import MultiAnimationRenderer
private final class CachedChatMessageText {
let text: String
@ -61,7 +64,7 @@ private final class InlineStickerItem: Hashable {
}
}
private final class InlineStickerItemLayer: SimpleLayer {
private final class InlineStickerItemLayer: MultiAnimationRenderTarget {
static let queue = Queue()
struct Key: Hashable {
@ -70,26 +73,50 @@ private final class InlineStickerItemLayer: SimpleLayer {
}
private let file: TelegramMediaFile
private let source: AnimatedStickerNodeSource
private var frameSource: QueueLocalObject<AnimatedStickerDirectFrameSource>?
//private var frameSource: QueueLocalObject<AnimatedStickerDirectFrameSource>?
private var disposable: Disposable?
private var fetchDisposable: Disposable?
private var isInHierarchyValue: Bool = false
var isVisibleForAnimations: Bool = false {
didSet {
if self.isVisibleForAnimations != oldValue {
self.updatePlayback()
}
}
}
private var displayLink: ConstantDisplayLinkAnimator?
init(context: AccountContext, file: TelegramMediaFile) {
self.source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false)
init(context: AccountContext, file: TelegramMediaFile, cache: AnimationCache, renderer: MultiAnimationRenderer) {
self.file = file
super.init()
let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
self.disposable = renderer.add(groupId: "inlineEmoji", target: self, cache: cache, itemId: file.resource.id.stringRepresentation, fetch: { writer in
let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false)
let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in
guard let result = result else {
return
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else {
writer.finish()
return
}
let scale = min(2.0, UIScreenScale)
cacheLottieAnimation(data: data, width: Int(24 * scale), height: Int(24 * scale), writer: writer)
})
let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start()
return ActionDisposable {
dataDisposable.dispose()
fetchDisposable.dispose()
}
})
/*let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
self.disposable = (self.source.directDataPath(attemptSynchronously: false)
|> filter { $0 != nil }
@ -109,18 +136,12 @@ private final class InlineStickerItemLayer: SimpleLayer {
}
})
self.fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start()
self.fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start()*/
}
override init(layer: Any) {
guard let layer = layer as? InlineStickerItemLayer else {
preconditionFailure()
}
self.source = layer.source
self.file = layer.file
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
@ -142,8 +163,11 @@ private final class InlineStickerItemLayer: SimpleLayer {
}
private func updatePlayback() {
let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations && self.frameSource != nil
if shouldBePlaying != (self.displayLink != nil) {
let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations
self.shouldBeAnimating = shouldBePlaying
/*if shouldBePlaying != (self.displayLink != nil) {
if shouldBePlaying {
self.displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.loadNextFrame()
@ -153,12 +177,10 @@ private final class InlineStickerItemLayer: SimpleLayer {
self.displayLink?.invalidate()
self.displayLink = nil
}
}
}*/
}
private var didRequestFrame = false
private func loadNextFrame() {
/*private func loadNextFrame() {
guard let frameSource = self.frameSource else {
return
}
@ -200,7 +222,7 @@ private final class InlineStickerItemLayer: SimpleLayer {
}
}
}
}
}*/
}
class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
@ -224,11 +246,25 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
override var visibility: ListViewItemNodeVisibility {
didSet {
let wasVisible = oldValue != .none
let isVisible = self.visibility != .none
if wasVisible != isVisible {
if !self.inlineStickerItemLayers.isEmpty {
if oldValue != self.visibility {
for (_, itemLayer) in self.inlineStickerItemLayers {
itemLayer.isVisibleForAnimations = isVisible
let isItemVisible: Bool
switch self.visibility {
case .none:
isItemVisible = false
case let .visible(_, subRect):
var subRect = subRect
subRect.origin.x = 0.0
subRect.size.width = 10000.0
if itemLayer.frame.intersects(subRect) {
isItemVisible = true
} else {
isItemVisible = false
}
}
itemLayer.isVisibleForAnimations = isItemVisible
}
}
}
}
@ -491,7 +527,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor
updatedAttributes[NSAttributedString.Key("Attribute__EmbeddedItem")] = InlineStickerItem(file: emojiFile)
let insertString = NSAttributedString(string: "[\u{00a0}\u{00a0}\u{00a0}]", attributes: updatedAttributes)
let insertString = NSAttributedString(string: "[\u{00a0}\u{00a0}]", attributes: updatedAttributes)
//updatedString.insert(insertString, at: NSRange(substringRange, in: updatedString.string).upperBound)
updatedString.replaceCharacters(in: range, with: insertString)
}
@ -687,7 +723,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.textAccessibilityOverlayNode.frame = textFrame
strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout
strongSelf.updateInlineStickers(context: item.context, textLayout: textLayout)
strongSelf.updateInlineStickers(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, textLayout: textLayout)
if let statusSizeAndApply = statusSizeAndApply {
animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: CGRect(origin: CGPoint(x: textFrameWithoutInsets.minX, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0), completion: nil)
@ -718,7 +754,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
}
private func updateInlineStickers(context: AccountContext, textLayout: TextNodeLayout?) {
private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?) {
var nextIndexById: [MediaId: Int] = [:]
var validIds: [InlineStickerItemLayer.Key] = []
@ -739,7 +775,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
if let current = self.inlineStickerItemLayers[id] {
itemLayer = current
} else {
itemLayer = InlineStickerItemLayer(context: context, file: stickerItem.file)
itemLayer = InlineStickerItemLayer(context: context, file: stickerItem.file, cache: cache, renderer: renderer)
self.inlineStickerItemLayers[id] = itemLayer
self.textNode.layer.addSublayer(itemLayer)
itemLayer.isVisibleForAnimations = self.isVisibleForAnimations

View File

@ -208,10 +208,10 @@ private func ensureThemeVisible(listNode: ListView, emoticon: String?, animated:
}
if let resultNode = resultNode {
var nodeToEnsure = resultNode
if case let .visible(resultVisibility) = resultNode.visibility, resultVisibility == 1.0 {
if let previousNode = previousNode, case let .visible(previousVisibility) = previousNode.visibility, previousVisibility < 0.5 {
if case let .visible(resultVisibility, _) = resultNode.visibility, resultVisibility == 1.0 {
if let previousNode = previousNode, case let .visible(previousVisibility, _) = previousNode.visibility, previousVisibility < 0.5 {
nodeToEnsure = previousNode
} else if let nextNode = nextNode, case let .visible(nextVisibility) = nextNode.visibility, nextVisibility < 0.5 {
} else if let nextNode = nextNode, case let .visible(nextVisibility, _) = nextNode.visibility, nextVisibility < 0.5 {
nodeToEnsure = nextNode
}
}

View File

@ -538,7 +538,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(backgroundNode: self.backgroundNode))
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode))
self.controllerInteraction = controllerInteraction
self.listNode.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in

View File

@ -189,10 +189,10 @@ private func ensureThemeVisible(listNode: ListView, emoticon: String?, animated:
}
if let resultNode = resultNode {
var nodeToEnsure = resultNode
if case let .visible(resultVisibility) = resultNode.visibility, resultVisibility == 1.0 {
if let previousNode = previousNode, case let .visible(previousVisibility) = previousNode.visibility, previousVisibility < 0.5 {
if case let .visible(resultVisibility, _) = resultNode.visibility, resultVisibility == 1.0 {
if let previousNode = previousNode, case let .visible(previousVisibility, _) = previousNode.visibility, previousVisibility < 0.5 {
nodeToEnsure = previousNode
} else if let nextNode = nextNode, case let .visible(nextVisibility) = nextNode.visibility, nextVisibility < 0.5 {
} else if let nextNode = nextNode, case let .visible(nextVisibility, _) = nextNode.visibility, nextVisibility < 0.5 {
nodeToEnsure = nextNode
}
}

View File

@ -164,7 +164,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: true), presentationContext: ChatPresentationContext(backgroundNode: nil))
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: true), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
self.blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))

View File

@ -155,7 +155,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, openWebView: { _, _, _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(backgroundNode: nil))
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)

View File

@ -2320,7 +2320,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(backgroundNode: nil))
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in
guard let strongSelf = self else {
return

View File

@ -1333,7 +1333,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(backgroundNode: backgroundNode as? WallpaperBackgroundNode))
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode))
var entryAttributes = ChatMessageEntryAttributes()
entryAttributes.isCentered = isCentered
@ -1462,8 +1462,6 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
}
private let defaultChatControllerInteraction = ChatControllerInteraction.default
private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {
if let _ = peer as? TelegramGroup {
return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, callMessages: [])

View File

@ -159,7 +159,10 @@ public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimate
count += 1
if count >= maxAnimatedEmojisInText {
#if DEBUG
#else
stop = true
#endif
}
}
}