646 lines
24 KiB
Objective-C
646 lines
24 KiB
Objective-C
//
|
||
// WRPageView.m
|
||
// WeRead (微信读书)
|
||
//
|
||
// Detailed pseudo-code reconstruction of the page rendering view.
|
||
// This view draws text directly via CoreText into CGContext — no UILabel
|
||
// or UITextView is used for the reading content.
|
||
//
|
||
|
||
#import "WRPageView.h"
|
||
#import "WRChapterData.h"
|
||
#import "WRCoreTextLayoutFrame.h"
|
||
#import "WRActivityIndicator.h"
|
||
#import "WRLoadingProgressView.h"
|
||
#import "WRFriendReviewsButton.h"
|
||
|
||
// ============================================================================
|
||
#pragma mark - Private Helpers
|
||
// ============================================================================
|
||
|
||
/// Extracts a plain-text NSString from a given range of the chapter's
|
||
/// attributed string, stripping any attachment characters.
|
||
static NSString *WRPlainStringFromRange(NSAttributedString *attrStr, NSRange range) {
|
||
if (!attrStr || range.location == NSNotFound) return @"";
|
||
NSString *raw = [[attrStr string] substringWithRange:range];
|
||
// Remove object replacement characters used for image attachments.
|
||
return [raw stringByReplacingOccurrencesOfString:@"" withString:@""];
|
||
}
|
||
|
||
/// Converts a UITouch point from view coordinates to the flipped coordinate
|
||
/// system expected by CoreText (origin at bottom-left).
|
||
static CGPoint WRSFlipPointForCoreText(CGPoint viewPoint, CGFloat viewHeight) {
|
||
return CGPointMake(viewPoint.x, viewHeight - viewPoint.y);
|
||
}
|
||
|
||
// ============================================================================
|
||
#pragma mark - WRPageView ()
|
||
// ============================================================================
|
||
|
||
@interface WRPageView ()
|
||
|
||
// ---- Private selection tracking ----
|
||
@property (nonatomic, assign) NSInteger selectionStartIndex;
|
||
@property (nonatomic, assign) NSInteger selectionEndIndex;
|
||
@property (nonatomic, assign) BOOL isSelecting;
|
||
|
||
// ---- Gesture recognizers ----
|
||
@property (nonatomic, strong) UITapGestureRecognizer *singleTapGR;
|
||
@property (nonatomic, strong) UILongPressGestureRecognizer *longPressGR;
|
||
@property (nonatomic, strong) UIPanGestureRecognizer *panGR;
|
||
|
||
// ---- Highlight/underline drawing cache ----
|
||
@property (nonatomic, strong) NSMutableArray<NSDictionary *> *highlightRects;
|
||
|
||
@end
|
||
|
||
// ============================================================================
|
||
#pragma mark - WRPageView Implementation
|
||
// ============================================================================
|
||
|
||
@implementation WRPageView
|
||
|
||
// ---- Synthesize properties backed by ivars from the binary ----
|
||
// The binary shows these ivar types:
|
||
// NSArray -> imageAttachments
|
||
// NSMutableArray -> visibleHighlights
|
||
// NSTimer -> autoReadTimer
|
||
// NSTimer -> loadingTimeoutTimer
|
||
// NSString -> chapterId
|
||
// UILabel -> headerLabel
|
||
// UIImageView -> backgroundImageView
|
||
// WRActivityIndicator -> activityIndicator
|
||
// UILabel -> statusLabel
|
||
// QMUIButton -> retryButton
|
||
// QMUIButton -> shareButton
|
||
// WRLoadingProgressView -> loadingProgressView
|
||
// QMUIButton -> bookmarkButton
|
||
// QMUIButton -> fontSizeUpButton
|
||
// QMUIButton -> fontSizeDownButton
|
||
// WRFriendReviewsButton -> friendReviewsButton
|
||
|
||
// ============================================================================
|
||
#pragma mark - Lifecycle
|
||
// ============================================================================
|
||
|
||
- (instancetype)initWithFrame:(CGRect)frame {
|
||
self = [super initWithFrame:frame];
|
||
if (self) {
|
||
// CoreText views must be opaque and have a cleared background for
|
||
// the CGContext to render correctly.
|
||
self.opaque = YES;
|
||
self.backgroundColor = [UIColor whiteColor];
|
||
self.clearsContextBeforeDrawing = YES;
|
||
|
||
// Enable multiple touches for selection handles.
|
||
self.multipleTouchEnabled = YES;
|
||
|
||
_selectionStartIndex = NSNotFound;
|
||
_selectionEndIndex = NSNotFound;
|
||
_isSelecting = NO;
|
||
|
||
[self _setupSubviews];
|
||
[self _setupGestureRecognizers];
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (void)dealloc {
|
||
[_autoReadTimer invalidate];
|
||
[_loadingTimeoutTimer invalidate];
|
||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||
}
|
||
|
||
// ============================================================================
|
||
#pragma mark - Subview Setup
|
||
// ============================================================================
|
||
|
||
/// Creates the non-text overlay UI elements. The reading text itself is
|
||
/// rendered entirely in -drawRect: via CoreText, so these are all floating
|
||
/// controls layered on top.
|
||
- (void)_setupSubviews {
|
||
// ---- Background image (e.g., paper texture) ----
|
||
_backgroundImageView = [[UIImageView alloc] initWithFrame:self.bounds];
|
||
_backgroundImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth |
|
||
UIViewAutoresizingFlexibleHeight;
|
||
_backgroundImageView.contentMode = UIViewContentModeScaleAspectFill;
|
||
[self addSubview:_backgroundImageView];
|
||
|
||
// ---- Header label (chapter title, page indicator) ----
|
||
_headerLabel = [[UILabel alloc] initWithFrame:CGRectZero];
|
||
_headerLabel.font = [UIFont systemFontOfSize:12.0];
|
||
_headerLabel.textColor = [UIColor grayColor];
|
||
_headerLabel.textAlignment = NSTextAlignmentCenter;
|
||
[self addSubview:_headerLabel];
|
||
|
||
// ---- Activity indicator (shown during chapter load) ----
|
||
_activityIndicator = [[WRActivityIndicator alloc] initWithFrame:CGRectZero];
|
||
_activityIndicator.hidden = YES;
|
||
[self addSubview:_activityIndicator];
|
||
|
||
// ---- Status label (error / empty state) ----
|
||
_statusLabel = [[UILabel alloc] initWithFrame:CGRectZero];
|
||
_statusLabel.font = [UIFont systemFontOfSize:15.0];
|
||
_statusLabel.textColor = [UIColor darkGrayColor];
|
||
_statusLabel.textAlignment = NSTextAlignmentCenter;
|
||
_statusLabel.numberOfLines = 0;
|
||
_statusLabel.hidden = YES;
|
||
[self addSubview:_statusLabel];
|
||
|
||
// ---- Retry button ----
|
||
_retryButton = [QMUIButton buttonWithType:UIButtonTypeSystem];
|
||
[_retryButton setTitle:@"重试" forState:UIControlStateNormal]; // "Retry"
|
||
[_retryButton addTarget:self
|
||
action:@selector(_retryButtonTapped:)
|
||
forControlEvents:UIControlEventTouchUpInside];
|
||
_retryButton.hidden = YES;
|
||
[self addSubview:_retryButton];
|
||
|
||
// ---- Share button ----
|
||
_shareButton = [QMUIButton buttonWithType:UIButtonTypeSystem];
|
||
[_shareButton setImage:[UIImage imageNamed:@"icon_share"]
|
||
forState:UIControlStateNormal];
|
||
[_shareButton addTarget:self
|
||
action:@selector(_shareButtonTapped:)
|
||
forControlEvents:UIControlEventTouchUpInside];
|
||
_shareButton.hidden = YES;
|
||
[self addSubview:_shareButton];
|
||
|
||
// ---- Loading progress view ----
|
||
_loadingProgressView = [[WRLoadingProgressView alloc] initWithFrame:CGRectZero];
|
||
_loadingProgressView.hidden = YES;
|
||
[self addSubview:_loadingProgressView];
|
||
|
||
// ---- Bookmark button ----
|
||
_bookmarkButton = [QMUIButton buttonWithType:UIButtonTypeSystem];
|
||
[_bookmarkButton addTarget:self
|
||
action:@selector(_bookmarkButtonTapped:)
|
||
forControlEvents:UIControlEventTouchUpInside];
|
||
[self addSubview:_bookmarkButton];
|
||
|
||
// ---- Font size buttons ----
|
||
_fontSizeUpButton = [QMUIButton buttonWithType:UIButtonTypeSystem];
|
||
[_fontSizeUpButton setTitle:@"A+" forState:UIControlStateNormal];
|
||
[_fontSizeUpButton addTarget:self
|
||
action:@selector(_fontSizeUpTapped:)
|
||
forControlEvents:UIControlEventTouchUpInside];
|
||
[self addSubview:_fontSizeUpButton];
|
||
|
||
_fontSizeDownButton = [QMUIButton buttonWithType:UIButtonTypeSystem];
|
||
[_fontSizeDownButton setTitle:@"A-" forState:UIControlStateNormal];
|
||
[_fontSizeDownButton addTarget:self
|
||
action:@selector(_fontSizeDownTapped:)
|
||
forControlEvents:UIControlEventTouchUpInside];
|
||
[self addSubview:_fontSizeDownButton];
|
||
|
||
// ---- Friend reviews button ----
|
||
_friendReviewsButton = [[WRFriendReviewsButton alloc] initWithFrame:CGRectZero];
|
||
[_friendReviewsButton addTarget:self
|
||
action:@selector(_friendReviewsTapped:)
|
||
forControlEvents:UIControlEventTouchUpInside];
|
||
_friendReviewsButton.hidden = YES;
|
||
[self addSubview:_friendReviewsButton];
|
||
}
|
||
|
||
// ============================================================================
|
||
#pragma mark - Gesture Recognizers
|
||
// ============================================================================
|
||
|
||
- (void)_setupGestureRecognizers {
|
||
// Single tap: link detection, selection dismissal, toolbar toggle.
|
||
_singleTapGR = [[UITapGestureRecognizer alloc]
|
||
initWithTarget:self action:@selector(_handleSingleTap:)];
|
||
_singleTapGR.numberOfTapsRequired = 1;
|
||
[self addGestureRecognizer:_singleTapGR];
|
||
|
||
// Long press: initiate text selection.
|
||
_longPressGR = [[UILongPressGestureRecognizer alloc]
|
||
initWithTarget:self action:@selector(_handleLongPress:)];
|
||
_longPressGR.minimumPressDuration = 0.5;
|
||
[self addGestureRecognizer:_longPressGR];
|
||
|
||
// Pan: extend selection after long press.
|
||
_panGR = [[UIPanGestureRecognizer alloc]
|
||
initWithTarget:self action:@selector(_handlePan:)];
|
||
_panGR.enabled = NO; // Enabled only when selecting.
|
||
[self addGestureRecognizer:_panGR];
|
||
|
||
// Long press should fail before single tap fires.
|
||
[_singleTapGR requireGestureRecognizerToFail:_longPressGR];
|
||
}
|
||
|
||
// ============================================================================
|
||
#pragma mark - Layout
|
||
// ============================================================================
|
||
|
||
- (void)layoutSubviews {
|
||
[super layoutSubviews];
|
||
// Position the header label at the top edge with padding.
|
||
CGFloat headerHeight = 20.0;
|
||
_headerLabel.frame = CGRectMake(16.0, 8.0,
|
||
CGRectGetWidth(self.bounds) - 32.0,
|
||
headerHeight);
|
||
|
||
// Center the activity indicator.
|
||
_activityIndicator.center = CGPointMake(CGRectGetMidX(self.bounds),
|
||
CGRectGetMidY(self.bounds));
|
||
|
||
// Position the friend reviews button at bottom-right.
|
||
CGSize reviewSize = CGSizeMake(60.0, 30.0);
|
||
_friendReviewsButton.frame =
|
||
CGRectMake(CGRectGetWidth(self.bounds) - reviewSize.width - 16.0,
|
||
CGRectGetHeight(self.bounds) - reviewSize.height - 40.0,
|
||
reviewSize.width, reviewSize.height);
|
||
}
|
||
|
||
// ============================================================================
|
||
#pragma mark - CoreText Drawing (drawRect:)
|
||
// ============================================================================
|
||
|
||
///
|
||
/// This is the heart of the page view. It draws the chapter text directly
|
||
/// into the CGContext using CoreText. No UILabel or UITextView is involved.
|
||
///
|
||
/// The flow is:
|
||
/// 1. Get the current graphics context.
|
||
/// 2. Flip the coordinate system for CoreText (origin at bottom-left).
|
||
/// 3. Call WRCoreTextLayoutFrame to draw the attributed string runs.
|
||
/// 4. Draw inline images at their attachment positions.
|
||
/// 5. Draw highlight/underline overlays.
|
||
///
|
||
- (void)drawRect:(CGRect)rect {
|
||
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
||
if (!ctx) return;
|
||
|
||
// If there is no chapter data or layout frame, just clear and return.
|
||
if (!self.chapterData || !self.layoutFrame) {
|
||
CGContextClearRect(ctx, rect);
|
||
return;
|
||
}
|
||
|
||
// ---- Step 1: Fill background ----
|
||
UIColor *bgColor = self.backgroundColor ?: [UIColor whiteColor];
|
||
CGContextSetFillColorWithColor(ctx, bgColor.CGColor);
|
||
CGContextFillRect(ctx, rect);
|
||
|
||
// ---- Step 2: Flip coordinate system for CoreText ----
|
||
// CoreText uses a bottom-left origin; UIKit uses top-left.
|
||
CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
|
||
CGContextTranslateCTM(ctx, 0.0, CGRectGetHeight(self.bounds));
|
||
CGContextScaleCTM(ctx, 1.0, -1.0);
|
||
|
||
// ---- Step 3: Draw the layout frame ----
|
||
// This calls WRCoreTextLayoutFrame -drawInContext:image:size:inRect:position:
|
||
// which iterates over CTLine objects, draws glyphs, and positions images.
|
||
CGSize pageSize = self.bounds.size;
|
||
CGPoint drawOrigin = CGPointMake(0.0, 0.0); // Could include margins.
|
||
CGRect drawRect = UIEdgeInsetsInsetRect(self.bounds,
|
||
self.chapterData.contentInsets);
|
||
|
||
[self.layoutFrame drawInContext:ctx
|
||
image:self.backgroundImageView.image
|
||
size:pageSize
|
||
inRect:drawRect
|
||
position:drawOrigin];
|
||
|
||
// ---- Step 4: Draw highlights and underlines ----
|
||
[self _drawHighlightsInContext:ctx];
|
||
|
||
// ---- Step 5: Draw selection handles if selecting ----
|
||
if (self.isSelecting) {
|
||
[self _drawSelectionInContext:ctx];
|
||
}
|
||
}
|
||
|
||
/// Draws highlight rectangles and underline paths for annotations,
|
||
/// bookmarks, and the current selection.
|
||
- (void)_drawHighlightsInContext:(CGContextRef)ctx {
|
||
for (NSDictionary *entry in self.highlightRects) {
|
||
CGRect hlRect = [entry[@"rect"] CGRectValue];
|
||
UIColor *color = entry[@"color"] ?: [UIColor yellowColor];
|
||
BOOL isUnderline = [entry[@"underline"] boolValue];
|
||
|
||
CGContextSetFillColorWithColor(ctx,
|
||
[color colorWithAlphaComponent:0.3].CGColor);
|
||
CGContextFillRect(ctx, hlRect);
|
||
|
||
if (isUnderline) {
|
||
CGFloat y = CGRectGetMaxY(hlRect);
|
||
CGContextSetStrokeColorWithColor(ctx, color.CGColor);
|
||
CGContextSetLineWidth(ctx, 1.0);
|
||
CGContextMoveToPoint(ctx, CGRectGetMinX(hlRect), y);
|
||
CGContextAddLineToPoint(ctx, CGRectGetMaxX(hlRect), y);
|
||
CGContextStrokePath(ctx);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Draws the selection highlight between selectionStartIndex and
|
||
/// selectionEndIndex using CTLineGetOffsetForStringIndex.
|
||
- (void)_drawSelectionInContext:(CGContextRef)ctx {
|
||
if (_selectionStartIndex == NSNotFound || _selectionEndIndex == NSNotFound) {
|
||
return;
|
||
}
|
||
|
||
// Clamp range.
|
||
NSInteger lo = MIN(_selectionStartIndex, _selectionEndIndex);
|
||
NSInteger hi = MAX(_selectionStartIndex, _selectionEndIndex);
|
||
NSRange selRange = NSMakeRange((NSUInteger)lo, (NSUInteger)(hi - lo));
|
||
|
||
// Ask the layout frame for the rects covering this range.
|
||
NSArray<NSValue *> *rects = [self.layoutFrame rectsWithinRange:selRange];
|
||
UIColor *selColor = [UIColor colorWithRed:0.2 green:0.5 blue:1.0 alpha:0.3];
|
||
|
||
for (NSValue *val in rects) {
|
||
CGRect r = [val CGRectValue];
|
||
CGContextSetFillColorWithColor(ctx, selColor.CGColor);
|
||
CGContextFillRect(ctx, r);
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
#pragma mark - Drawing API (called externally)
|
||
// ============================================================================
|
||
|
||
/// External entry point for drawing. Delegates to the layout frame.
|
||
- (void)drawInContext:(CGContextRef)context
|
||
withData:(WRChapterData *)chapterData
|
||
size:(CGSize)size
|
||
inRect:(CGRect)rect
|
||
position:(CGPoint)position {
|
||
// Store references so -drawRect: can use them.
|
||
self.chapterData = chapterData;
|
||
|
||
// Trigger a redraw.
|
||
[self setNeedsDisplay];
|
||
}
|
||
|
||
// ============================================================================
|
||
#pragma mark - Text Selection via CoreText Hit Testing
|
||
// ============================================================================
|
||
|
||
/// Converts a view-coordinate point to a string index in the attributed string
|
||
/// using CTLineGetStringIndexForPosition.
|
||
- (NSInteger)stringIndexForPoint:(CGPoint)point {
|
||
if (!self.layoutFrame) return NSNotFound;
|
||
|
||
// Flip Y for CoreText.
|
||
CGPoint ctPoint = WRSFlipPointForCoreText(point, CGRectGetHeight(self.bounds));
|
||
|
||
// Walk the CTLine objects in the layout frame to find the line at this Y,
|
||
// then use CTLineGetStringIndexForPosition to get the character index.
|
||
NSInteger index = [self.layoutFrame stringIndexForPoint:ctPoint];
|
||
return index;
|
||
}
|
||
|
||
/// Returns the range of the line containing the given string index,
|
||
/// using CTLineGetOffsetForStringIndex to find line boundaries.
|
||
- (NSRange)lineRangeForStringIndex:(NSInteger)index {
|
||
if (!self.layoutFrame || index == NSNotFound) {
|
||
return NSMakeRange(NSNotFound, 0);
|
||
}
|
||
return [self.layoutFrame lineRangeForStringIndex:index];
|
||
}
|
||
|
||
/// Clears the current text selection and disables the pan gesture.
|
||
- (void)clearSelection {
|
||
_selectionStartIndex = NSNotFound;
|
||
_selectionEndIndex = NSNotFound;
|
||
_isSelecting = NO;
|
||
_panGR.enabled = NO;
|
||
[self setNeedsDisplay];
|
||
}
|
||
|
||
// ============================================================================
|
||
#pragma mark - Gesture Handlers
|
||
// ============================================================================
|
||
|
||
- (void)_handleSingleTap:(UITapGestureRecognizer *)gr {
|
||
CGPoint pt = [gr locationInView:self];
|
||
|
||
// If currently selecting, dismiss selection.
|
||
if (self.isSelecting) {
|
||
[self clearSelection];
|
||
return;
|
||
}
|
||
|
||
// Check if the tap hits a link in the layout frame.
|
||
NSInteger idx = [self stringIndexForPoint:pt];
|
||
if (idx != NSNotFound) {
|
||
NSURL *linkURL = [self.layoutFrame linkURLAtIndex:idx];
|
||
if (linkURL) {
|
||
if ([self.delegate respondsToSelector:@selector(pageView:didTapLinkWithURL:)]) {
|
||
[self.delegate pageView:self didTapLinkWithURL:linkURL];
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Otherwise, toggle toolbar / delegate the tap.
|
||
// (In the real app, this toggles the reader chrome.)
|
||
}
|
||
|
||
- (void)_handleLongPress:(UILongPressGestureRecognizer *)gr {
|
||
if (gr.state == UIGestureRecognizerStateBegan) {
|
||
CGPoint pt = [gr locationInView:self];
|
||
NSInteger idx = [self stringIndexForPoint:pt];
|
||
|
||
if (idx != NSNotFound) {
|
||
_selectionStartIndex = idx;
|
||
_selectionEndIndex = idx;
|
||
_isSelecting = YES;
|
||
_panGR.enabled = YES;
|
||
[self setNeedsDisplay];
|
||
|
||
if ([self.delegate respondsToSelector:@selector(pageView:didBeginSelectionAtPoint:)]) {
|
||
[self.delegate pageView:self didBeginSelectionAtPoint:pt];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)_handlePan:(UIPanGestureRecognizer *)gr {
|
||
if (!self.isSelecting) return;
|
||
|
||
CGPoint pt = [gr locationInView:self];
|
||
NSInteger idx = [self stringIndexForPoint:pt];
|
||
|
||
if (idx != NSNotFound && idx != _selectionEndIndex) {
|
||
_selectionEndIndex = idx;
|
||
[self setNeedsDisplay];
|
||
|
||
// Notify delegate of selection range change.
|
||
NSInteger lo = MIN(_selectionStartIndex, _selectionEndIndex);
|
||
NSInteger hi = MAX(_selectionStartIndex, _selectionEndIndex);
|
||
NSRange selRange = NSMakeRange((NSUInteger)lo, (NSUInteger)(hi - lo));
|
||
|
||
if ([self.delegate respondsToSelector:@selector(pageView:selectionDidChangeWithRange:)]) {
|
||
[self.delegate pageView:self selectionDidChangeWithRange:selRange];
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
#pragma mark - Accessibility
|
||
// ============================================================================
|
||
|
||
/// Provides a plain-text representation of the page for VoiceOver.
|
||
- (nullable NSString *)accessibilityValue {
|
||
if (!self.chapterData) return nil;
|
||
NSAttributedString *attrStr = self.chapterData.typesetAttributedString;
|
||
if (!attrStr) return nil;
|
||
|
||
// Return the plain text for the current page range.
|
||
NSRange pageRange = [self.chapterData rangeOfPage:self.pageIndex];
|
||
return WRPlainStringFromRange(attrStr, pageRange);
|
||
}
|
||
|
||
/// Supports VoiceOver scroll gestures to flip pages.
|
||
- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
|
||
// Post a page-change notification for the WRPageViewController to handle.
|
||
NSString *dirStr = nil;
|
||
switch (direction) {
|
||
case UIAccessibilityScrollDirectionLeft:
|
||
dirStr = @"next";
|
||
break;
|
||
case UIAccessibilityScrollDirectionRight:
|
||
dirStr = @"previous";
|
||
break;
|
||
default:
|
||
return NO;
|
||
}
|
||
|
||
[[NSNotificationCenter defaultCenter]
|
||
postNotificationName:@"WRPageViewAccessibilityScroll"
|
||
object:self
|
||
userInfo:@{@"direction": dirStr}];
|
||
return YES;
|
||
}
|
||
|
||
// ============================================================================
|
||
#pragma mark - Friend Reviews
|
||
// ============================================================================
|
||
|
||
- (void)friendReviewsCount:(NSUInteger)count {
|
||
self.friendReviewsButton.hidden = (count == 0);
|
||
[self.friendReviewsButton setReviewCount:count];
|
||
}
|
||
|
||
// ============================================================================
|
||
#pragma mark - Loading / Error States
|
||
// ============================================================================
|
||
|
||
- (void)showLoadingWithProgress:(float)progress {
|
||
self.activityIndicator.hidden = NO;
|
||
[self.activityIndicator startAnimating];
|
||
|
||
if (progress > 0.0 && progress < 1.0) {
|
||
self.loadingProgressView.hidden = NO;
|
||
self.loadingProgressView.progress = progress;
|
||
} else {
|
||
self.loadingProgressView.hidden = YES;
|
||
}
|
||
|
||
// Start a timeout timer — if loading takes too long, show an error.
|
||
[_loadingTimeoutTimer invalidate];
|
||
_loadingTimeoutTimer =
|
||
[NSTimer scheduledTimerWithTimeInterval:15.0
|
||
target:self
|
||
selector:@selector(_loadingDidTimeout)
|
||
userInfo:nil
|
||
repeats:NO];
|
||
}
|
||
|
||
- (void)hideLoading {
|
||
[_loadingTimeoutTimer invalidate];
|
||
_loadingTimeoutTimer = nil;
|
||
|
||
[self.activityIndicator stopAnimating];
|
||
self.activityIndicator.hidden = YES;
|
||
self.loadingProgressView.hidden = YES;
|
||
}
|
||
|
||
- (void)showErrorWithMessage:(NSString *)message {
|
||
[self hideLoading];
|
||
self.statusLabel.text = message;
|
||
self.statusLabel.hidden = NO;
|
||
self.retryButton.hidden = NO;
|
||
}
|
||
|
||
- (void)_loadingDidTimeout {
|
||
[self showErrorWithMessage:@"加载超时,请重试"]; // "Loading timed out, please retry"
|
||
}
|
||
|
||
// ============================================================================
|
||
#pragma mark - Button Actions
|
||
// ============================================================================
|
||
|
||
- (void)_retryButtonTapped:(QMUIButton *)sender {
|
||
self.statusLabel.hidden = YES;
|
||
self.retryButton.hidden = YES;
|
||
if ([self.delegate respondsToSelector:@selector(pageViewNeedsReload:)]) {
|
||
[self.delegate pageViewNeedsReload:self];
|
||
}
|
||
}
|
||
|
||
- (void)_shareButtonTapped:(QMUIButton *)sender {
|
||
// Share current page content.
|
||
// Handled by delegate or responder chain.
|
||
}
|
||
|
||
- (void)_bookmarkButtonTapped:(QMUIButton *)sender {
|
||
// Toggle bookmark for current page.
|
||
sender.selected = !sender.selected;
|
||
}
|
||
|
||
- (void)_fontSizeUpTapped:(QMUIButton *)sender {
|
||
// Increase font size — triggers re-typeset.
|
||
[[NSNotificationCenter defaultCenter]
|
||
postNotificationName:@"WRPageViewFontSizeChange"
|
||
object:self
|
||
userInfo:@{@"delta": @(1)}];
|
||
}
|
||
|
||
- (void)_fontSizeDownTapped:(QMUIButton *)sender {
|
||
[[NSNotificationCenter defaultCenter]
|
||
postNotificationName:@"WRPageViewFontSizeChange"
|
||
object:self
|
||
userInfo:@{@"delta": @(-1)}];
|
||
}
|
||
|
||
- (void)_friendReviewsTapped:(QMUIButton *)sender {
|
||
if ([self.delegate respondsToSelector:@selector(pageView:didTapFriendReviewsWithCount:)]) {
|
||
[self.delegate pageView:self
|
||
didTapFriendReviewsWithCount:sender.tag];
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
#pragma mark - Auto-Read Timer
|
||
// ============================================================================
|
||
|
||
/// Starts the auto-read timer that periodically advances the page.
|
||
- (void)startAutoReadWithInterval:(NSTimeInterval)interval {
|
||
[_autoReadTimer invalidate];
|
||
_autoReadTimer =
|
||
[NSTimer scheduledTimerWithTimeInterval:interval
|
||
target:self
|
||
selector:@selector(_autoReadTick)
|
||
userInfo:nil
|
||
repeats:YES];
|
||
}
|
||
|
||
- (void)stopAutoRead {
|
||
[_autoReadTimer invalidate];
|
||
_autoReadTimer = nil;
|
||
}
|
||
|
||
- (void)_autoReadTick {
|
||
// Post notification for the page view controller to advance.
|
||
[[NSNotificationCenter defaultCenter]
|
||
postNotificationName:@"WRPageViewAutoReadAdvance"
|
||
object:self];
|
||
}
|
||
|
||
@end
|