Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/photo_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ library photo_view;
import 'package:flutter/material.dart';

import 'package:photo_view/src/controller/photo_view_controller.dart';
import 'package:photo_view/src/controller/photo_view_controller_base.dart';
import 'package:photo_view/src/controller/photo_view_scalestate_controller.dart';
import 'package:photo_view/src/core/photo_view_core.dart';
import 'package:photo_view/src/photo_view_computed_scale.dart';
Expand All @@ -11,7 +12,9 @@ import 'package:photo_view/src/photo_view_wrappers.dart';
import 'package:photo_view/src/utils/photo_view_hero_attributes.dart';

export 'src/controller/photo_view_controller.dart';
export 'src/controller/photo_view_controller_base.dart';
export 'src/controller/photo_view_scalestate_controller.dart';
export 'src/core/photo_view_animation_delegate.dart';
export 'src/core/photo_view_gesture_detector.dart'
show PhotoViewGestureDetectorScope;
export 'src/photo_view_computed_scale.dart';
Expand Down
185 changes: 77 additions & 108 deletions lib/src/controller/photo_view_controller.dart
Original file line number Diff line number Diff line change
@@ -1,116 +1,10 @@
import 'dart:async';

import 'package:flutter/widgets.dart';
import 'package:photo_view/src/controller/photo_view_controller_base.dart';
import 'package:photo_view/src/controller/photo_view_controller_delegate.dart';
import 'package:photo_view/src/utils/ignorable_change_notifier.dart';

/// The interface in which controllers will be implemented.
///
/// It concerns storing the state ([PhotoViewControllerValue]) and streaming its updates.
/// [PhotoViewImageWrapper] will respond to user gestures setting thew fields in the instance of a controller.
///
/// Any instance of a controller must be disposed after unmount. So if you instantiate a [PhotoViewController] or your custom implementation, do not forget to dispose it when not using it anymore.
///
/// The controller exposes value fields like [scale] or [rotationFocus]. Usually those fields will be only getters and setters serving as hooks to the internal [PhotoViewControllerValue].
///
/// The default implementation used by [PhotoView] is [PhotoViewController].
///
/// This was created to allow customization (you can create your own controller class)
///
/// Previously it controlled `scaleState` as well, but duw to some [concerns](https://github.com/renancaraujo/photo_view/issues/127)
/// [ScaleStateListener is responsible for tat value now
///
/// As it is a controller, whoever instantiates it, should [dispose] it afterwards.
///
abstract class PhotoViewControllerBase<T extends PhotoViewControllerValue> {
/// The output for state/value updates. Usually a broadcast [Stream]
Stream<T> get outputStateStream;

/// The state value before the last change or the initial state if the state has not been changed.
late T prevValue;

/// The actual state value
late T value;

/// Resets the state to the initial value;
void reset();

/// Closes streams and removes eventual listeners.
void dispose();

/// Add a listener that will ignore updates made internally
///
/// Since it is made for internal use, it is not performatic to use more than one
/// listener. Prefer [outputStateStream]
void addIgnorableListener(VoidCallback callback);

/// Remove a listener that will ignore updates made internally
///
/// Since it is made for internal use, it is not performatic to use more than one
/// listener. Prefer [outputStateStream]
void removeIgnorableListener(VoidCallback callback);

/// The position of the image in the screen given its offset after pan gestures.
late Offset position;

/// The scale factor to transform the child (image or a customChild).
late double? scale;

/// Nevermind this method :D, look away
void setScaleInvisibly(double? scale);

/// The rotation factor to transform the child (image or a customChild).
late double rotation;

/// The center of the rotation transformation. It is a coordinate referring to the absolute dimensions of the image.
Offset? rotationFocusPoint;

/// Update multiple fields of the state with only one update streamed.
void updateMultiple({
Offset? position,
double? scale,
double? rotation,
Offset? rotationFocusPoint,
});
}

/// The state value stored and streamed by [PhotoViewController].
@immutable
class PhotoViewControllerValue {
const PhotoViewControllerValue({
required this.position,
required this.scale,
required this.rotation,
required this.rotationFocusPoint,
});

final Offset position;
final double? scale;
final double rotation;
final Offset? rotationFocusPoint;

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PhotoViewControllerValue &&
runtimeType == other.runtimeType &&
position == other.position &&
scale == other.scale &&
rotation == other.rotation &&
rotationFocusPoint == other.rotationFocusPoint;

@override
int get hashCode =>
position.hashCode ^
scale.hashCode ^
rotation.hashCode ^
rotationFocusPoint.hashCode;

@override
String toString() {
return 'PhotoViewControllerValue{position: $position, scale: $scale, rotation: $rotation, rotationFocusPoint: $rotationFocusPoint}';
}
}

/// The default implementation of [PhotoViewControllerBase].
///
/// Containing a [ValueNotifier] it stores the state in the [value] field and streams
Expand Down Expand Up @@ -147,6 +41,11 @@ class PhotoViewController

late StreamController<PhotoViewControllerValue> _outputCtrl;

PhotoViewAnimationDelegate? _delegate;

/// Queue for commands triggered before the controller is attached
final List<_ControllerCommand> _pendingCommands = [];

@override
Stream<PhotoViewControllerValue> get outputStateStream => _outputCtrl.stream;

Expand All @@ -156,6 +55,7 @@ class PhotoViewController
@override
void reset() {
value = initial;
_pendingCommands.clear();
}

void _changeListener() {
Expand Down Expand Up @@ -288,4 +188,73 @@ class PhotoViewController
}
_valueNotifier.value = newValue;
}

@override
void attach(PhotoViewAnimationDelegate delegate) {
_delegate = delegate;
// Execute any pending commands now that we are attached
for (final command in _pendingCommands) {
command.execute(delegate);
}
_pendingCommands.clear();
}

@override
void detach() {
_delegate = null;
}

/// Triggers a smooth, physics-based zoom by the given [factor].
///
/// If the controller is not yet attached to a [PhotoView], the command
/// is queued and executed immediately upon attachment.
void animateScaleBy({required double factor, Offset? focalPoint}) {
final delegate = _delegate;
if (delegate != null) {
delegate.animateScaleBy(factor: factor, focalPoint: focalPoint);
} else {
_pendingCommands.add(_ScaleByCommand(factor, focalPoint));
}
}

/// Triggers a smooth, physics-based pan by the given [delta].
///
/// If the controller is not yet attached to a [PhotoView], the command
/// is queued and executed immediately upon attachment.
void animatePositionBy({required Offset delta}) {
final delegate = _delegate;
if (delegate != null) {
delegate.animatePositionBy(delta: delta);
} else {
_pendingCommands.add(_PositionByCommand(delta));
}
}
}

/// A command object to store pending animation requests
abstract class _ControllerCommand {
void execute(PhotoViewAnimationDelegate delegate);
}

class _ScaleByCommand implements _ControllerCommand {
const _ScaleByCommand(this.factor, this.focalPoint);

final double factor;
final Offset? focalPoint;

@override
void execute(PhotoViewAnimationDelegate delegate) {
delegate.animateScaleBy(factor: factor, focalPoint: focalPoint);
}
}

class _PositionByCommand implements _ControllerCommand {
const _PositionByCommand(this.delta);

final Offset delta;

@override
void execute(PhotoViewAnimationDelegate delegate) {
delegate.animatePositionBy(delta: delta);
}
}
119 changes: 119 additions & 0 deletions lib/src/controller/photo_view_controller_base.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import 'dart:async';

import 'package:flutter/widgets.dart';
import 'package:photo_view/src/controller/photo_view_controller_delegate.dart';

/// The interface in which controllers will be implemented.
///
/// It concerns storing the state ([PhotoViewControllerValue]) and streaming its updates.
/// [PhotoViewImageWrapper] will respond to user gestures setting thew fields in the instance of a controller.
///
/// Any instance of a controller must be disposed after unmount. So if you instantiate a [PhotoViewController] or your custom implementation, do not forget to dispose it when not using it anymore.
///
/// The controller exposes value fields like [scale] or [rotationFocus]. Usually those fields will be only getters and setters serving as hooks to the internal [PhotoViewControllerValue].
///
/// The default implementation used by [PhotoView] is [PhotoViewController].
///
/// This was created to allow customization (you can create your own controller class)
///
/// Previously it controlled `scaleState` as well, but duw to some [concerns](https://github.com/renancaraujo/photo_view/issues/127)
/// [ScaleStateListener is responsible for tat value now
///
/// As it is a controller, whoever instantiates it, should [dispose] it afterwards.
///
abstract class PhotoViewControllerBase<T extends PhotoViewControllerValue> {
/// The output for state/value updates. Usually a broadcast [Stream]
Stream<T> get outputStateStream;

/// The state value before the last change or the initial state if the state has not been changed.
late T prevValue;

/// The actual state value
late T value;

/// Resets the state to the initial value;
void reset();

/// Closes streams and removes eventual listeners.
void dispose();

/// Add a listener that will ignore updates made internally
///
/// Since it is made for internal use, it is not performatic to use more than one
/// listener. Prefer [outputStateStream]
void addIgnorableListener(VoidCallback callback);

/// Remove a listener that will ignore updates made internally
///
/// Since it is made for internal use, it is not performatic to use more than one
/// listener. Prefer [outputStateStream]
void removeIgnorableListener(VoidCallback callback);

/// The position of the image in the screen given its offset after pan gestures.
late Offset position;

/// The scale factor to transform the child (image or a customChild).
late double? scale;

/// Nevermind this method :D, look away
void setScaleInvisibly(double? scale);

/// The rotation factor to transform the child (image or a customChild).
late double rotation;

/// The center of the rotation transformation. It is a coordinate referring to the absolute dimensions of the image.
Offset? rotationFocusPoint;

/// Update multiple fields of the state with only one update streamed.
void updateMultiple({
Offset? position,
double? scale,
double? rotation,
Offset? rotationFocusPoint,
});

/// Attaches an animation delegate to this controller.
/// This is used internally by [PhotoViewCore] to enable physics-based animations.
void attach(PhotoViewAnimationDelegate delegate);

/// Detaches the animation delegate.
void detach();
}

/// The state value stored and streamed by [PhotoViewController].
@immutable
class PhotoViewControllerValue {
const PhotoViewControllerValue({
required this.position,
required this.scale,
required this.rotation,
required this.rotationFocusPoint,
});

final Offset position;
final double? scale;
final double rotation;
final Offset? rotationFocusPoint;

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PhotoViewControllerValue &&
runtimeType == other.runtimeType &&
position == other.position &&
scale == other.scale &&
rotation == other.rotation &&
rotationFocusPoint == other.rotationFocusPoint;

@override
int get hashCode =>
position.hashCode ^
scale.hashCode ^
rotation.hashCode ^
rotationFocusPoint.hashCode;

@override
String toString() {
return 'PhotoViewControllerValue{position: $position, scale: $scale, rotation: $rotation, rotationFocusPoint: $rotationFocusPoint}';
}
}
16 changes: 16 additions & 0 deletions lib/src/controller/photo_view_controller_delegate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ import 'package:photo_view/src/core/photo_view_core.dart';
import 'package:photo_view/src/photo_view_scale_state.dart';
import 'package:photo_view/src/utils/photo_view_utils.dart';

/// An interface used by the Controller to delegate animation requests to the Core.
///
/// This allows the controller to trigger complex, physics-based animations
/// (like smooth zooming or panning) that require knowledge of the layout/ticker,
/// which resides in the [PhotoViewCore].
abstract class PhotoViewAnimationDelegate {
/// Smoothly zooms the content by a specific [factor] (e.g. 1.1 for +10%).
///
/// [focalPoint] is the pixel location on the screen that remains stationary
/// during the zoom. If null, the center of the viewport is used.
void animateScaleBy({required double factor, Offset? focalPoint});

/// Smoothly pans the content by a specific [delta] offset.
void animatePositionBy({required Offset delta});
}

/// A class to hold internal layout logic to sync both controller states
///
/// It reacts to layout changes (eg: enter landscape or widget resize) and syncs the two controllers.
Expand Down
5 changes: 5 additions & 0 deletions lib/src/core/photo_view_animation_delegate.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import 'package:flutter/widgets.dart';

abstract class PhotoViewAnimationDelegate {
void animateScaleBy({required double factor, Offset? focalPoint});
}
Loading