>

RikuloRikulo

Blog

Back to index


A Photo Album Prototype with Swipe Gesture

The Plan

We are going to build an album-like demo as an application of the new SwipeGesture. If you are a front end web developer, you should be familiar with the construction:

Then we are going to wire up a SwipeGesture and a Motion, so that the pictures will shift when they receive a swipe gesture.

The Outcome

Swipe to view next or previous photo.

Layout and Responsive Design

We start by laying down the views:

final View frame = new View();
frame.style.overflow = "hidden";
frame.profile.location = "center center";
// frame width/height = minimum of [window width] and [window height]

final View frameInner = new View();
// frameInner height = [frame height]
// frameInner width = [frame width] * [photo count]
// frameInner top = 0 (default)
// frameInner left = - [photo index] * [frame width]
frame.addChild(frameInner);

for (int i = 0; i < photoCount; i++) {
    View photoBox = new View();
    // photoBox width/height = [frame width/height] - 50, capped at 500
    // photoBox top = an offset which makes it center-aligned to frame
    // photoBox left = [the offset] + [photo index] * [frame width/height]

    Image photo = new Image();
    photo.classes.add("photo");
    photo.profile.text = "location: top left; width: 100%; height: 100%";
    photo.src = "res/alpaca-0${i+1}.jpg";

    View mask = new View(); // to block browser's default image dragging
    mask.classes.add("photo-mask");
    mask.profile.text = "location: top left; width: 100%; height: 100%";

    photoBox.addChild(photo);
    photoBox.addChild(mask);
    frameInner.addChild(photoBox);
}

mainView.addChild(frame);

Note that the size settings of some views are purposely left out, as we are going to handle it directly within the layout callback, so they will be responsive automatically:

frame.on.preLayout.add((LayoutEvent event) {
    final Size msize = new DOMQuery(mainView).innerSize;
    frameSize = min(msize.width, msize.height);
    final int photoSize = min(frameSize - 50, 500);
    final int photoOffset = ((frameSize - photoSize) / 2).toInt();

    frame.width = frame.height = frameInner.height = frameSize;
    frameInner.width = frameSize * photoCount;
    frameInner.left = -_index * frameSize;

    for (int i = 0; i < photoCount; i++) {
        View photoBox = frameInner.children[i];
        photoBox.width = photoBox.height = photoSize;
        photoBox.left = photoOffset + i * frameSize;
        photoBox.top = photoOffset;
    }
});

Business Logic

Now we are going to write down the business logic:

int _index = 0;

void next() => select(_index + 1);

void previous() => select(_index - 1);

void select(int index) {
    if (index < 0 || index >= photoCount) {
        // do nothing
        return;
    }
    // TODO: trigger an animation to shift the photos
}

Animation

The animation part is straightforward:

void select(int index) {
    if (index < 0 || index >= photoCount) {
        // do nothing
        return;
    }
    // trigger an animation to shift the photos
    final Offset origin = new Offset(-_index * frameSize, 0);
    final Offset dest = new Offset(-index * frameSize, 0);
    new LinearPathMotion(frameInner.node, origin, dest, end: (MotionState state) {
        _index = index;
    }, easing: (num x) => x * x);
}

Swipe Gesture

Here we are going to capture user's swipe gesture.

new SwipeGesture(mainView.node, (SwipeGestureState state) {
    final int diff = state.delta.x;
    if (diff < -50) // swipe left
        next();
    else if (diff > 50) // swipe right
        previous();
});

But wait! There is an issue with this implementation. Whenever there are multiple gestures/motions in the game, it is necessary to consider whether they will conflict each other. Generally there will be a priority, where one gesture/motion will block or interrupt another.

In the previous example we have shown how a gesture interrupts a motion. In this scenario, we are going to make the photo-shifting motion block the incoming swipe gesture:

SwipeGesture gesture;

gesture = new SwipeGesture(mainView.node, (SwipeGestureState state) {
    gesture.disable();
    final int diff = state.delta.x;
    if (diff < -50) // swipe left
        next();
    else if (diff > 50) // swipe right
        previous();
    else
        gesture.enable();
});

void select(int index) {
    if (index < 0 || index >= photoCount) {
        gesture.enable();
        return;
    }
    final Offset origin = new Offset(-_index * frameSize, 0);
    final Offset dest = new Offset(-index * frameSize, 0);
    new LinearPathMotion(frameInner.node, origin, dest, end: (MotionState state) {
        _index = index;
        gesture.enable();
    }, easing: (num x) => x * x);
}

The gestures come with enable/disable APIs, allowing us to easily turn it on and off.

Conclusion

At the end we added a few more decorations to the demo to make it fancier. They are quite straightforward to figure out from the source code.

comments powered by Disqus

CATEGORIES