Commit b2e8a52c authored by Mehran Mahmoudi's avatar Mehran Mahmoudi Committed by Commit Bot

[Paint Preview] Player: Implement UI for frames + scrolling

This implements the player UI using a hierarchy of Views. This enables
displaying the main the entire hierarchy of sub-frames. It also adds
support for scrolling in both directions for each frame.

Each Paint Preview frame is represented a PlayerFrame* sub-component
(everything in the org.chromium.components.paintpreview.player.frame
package). The approach is explained in more details on the design
doc linked below.

Internal design doc: http://go/fdt-player-2

Bug: 1021202,1020700
Change-Id: I7cc5310016c614f57105105a10d8db7a98e97654
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1925052Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Reviewed-by: default avatarMatthew Jones <mdjones@chromium.org>
Commit-Queue: Mehran Mahmoudi <mahmoudi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#721246}
parent 5218be19
...@@ -54,17 +54,29 @@ android_library("java") { ...@@ -54,17 +54,29 @@ android_library("java") {
"java/src/org/chromium/components/paintpreview/player/PlayerCompositorDelegate.java", "java/src/org/chromium/components/paintpreview/player/PlayerCompositorDelegate.java",
"java/src/org/chromium/components/paintpreview/player/PlayerCompositorDelegateImpl.java", "java/src/org/chromium/components/paintpreview/player/PlayerCompositorDelegateImpl.java",
"java/src/org/chromium/components/paintpreview/player/PlayerManager.java", "java/src/org/chromium/components/paintpreview/player/PlayerManager.java",
"java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameBitmapPainter.java",
"java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameCoordinator.java", "java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameCoordinator.java",
"java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameGestureDetector.java",
"java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameMediator.java",
"java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameProperties.java",
"java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameView.java",
"java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameViewBinder.java",
"java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameViewDelegate.java",
] ]
deps = [ deps = [
"//base:base_java", "//base:base_java",
"//base:jni_java", "//base:jni_java",
"//ui/android:ui_java",
] ]
} }
junit_binary("paint_preview_junit_tests") { junit_binary("paint_preview_junit_tests") {
java_files = [ "junit/src/org/chromium/components/paintpreview/player/PlayerManagerTest.java" ] java_files = [
"junit/src/org/chromium/components/paintpreview/player/PlayerManagerTest.java",
"junit/src/org/chromium/components/paintpreview/player/frame/PlayerFrameBitmapPainterTest.java",
"junit/src/org/chromium/components/paintpreview/player/frame/PlayerFrameMediatorTest.java",
]
deps = [ deps = [
":java", ":java",
"//base:base_java", "//base:base_java",
......
include_rules = [
"+ui/android",
]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.paintpreview.player.frame;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import javax.annotation.Nonnull;
/**
* Given a viewport {@link Rect} and a matrix of {@link Bitmap} tiles, this class draws the bitmaps
* on a {@link Canvas}.
*/
class PlayerFrameBitmapPainter {
private Bitmap[][] mBitmapMatrix;
private Rect mViewPort = new Rect();
private Rect mDrawBitmapSrc = new Rect();
private Rect mDrawBitmapDst = new Rect();
private Runnable mInvalidateCallback;
PlayerFrameBitmapPainter(@Nonnull Runnable invalidateCallback) {
mInvalidateCallback = invalidateCallback;
}
void updateViewPort(int left, int top, int right, int bottom) {
mViewPort.set(left, top, right, bottom);
mInvalidateCallback.run();
}
void updateBitmapMatrix(Bitmap[][] bitmapMatrix) {
mBitmapMatrix = bitmapMatrix;
mInvalidateCallback.run();
}
/**
* Draws bitmaps on a given {@link Canvas} for the current viewport.
*/
void onDraw(Canvas canvas) {
if (mBitmapMatrix == null) {
return;
}
if (mViewPort.isEmpty()) {
return;
}
final int tileHeight = mViewPort.height();
final int tileWidth = mViewPort.width();
final int rowStart = mViewPort.top / tileHeight;
final int rowEnd = (int) Math.ceil((double) mViewPort.bottom / tileHeight);
final int colStart = mViewPort.left / tileWidth;
final int colEnd = (int) Math.ceil((double) mViewPort.right / tileWidth);
if (rowEnd > mBitmapMatrix.length || colEnd > mBitmapMatrix[rowEnd - 1].length) {
return;
}
for (int row = rowStart; row < rowEnd; row++) {
for (int col = colStart; col < colEnd; col++) {
Bitmap tileBitmap = mBitmapMatrix[row][col];
// Calculate the portion of this tileBitmap that is visible in mViewPort.
int bitmapLeft = Math.max(mViewPort.left - (col * tileWidth), 0);
int bitmapTop = Math.max(mViewPort.top - (row * tileHeight), 0);
int bitmapRight =
Math.min(tileWidth, bitmapLeft + mViewPort.right - (col * tileWidth));
int bitmapBottom =
Math.min(tileHeight, bitmapTop + mViewPort.bottom - (row * tileHeight));
mDrawBitmapSrc.set(bitmapLeft, bitmapTop, bitmapRight, bitmapBottom);
// Calculate the portion of the canvas that tileBitmap is gonna be drawn on.
int canvasLeft = Math.max((col * tileWidth) - mViewPort.left, 0);
int canvasTop = Math.max((row * tileHeight) - mViewPort.top, 0);
int canvasRight = canvasLeft + mDrawBitmapSrc.width();
int canvasBottom = canvasTop + mDrawBitmapSrc.height();
mDrawBitmapDst.set(canvasLeft, canvasTop, canvasRight, canvasBottom);
canvas.drawBitmap(tileBitmap, mDrawBitmapSrc, mDrawBitmapDst, null);
}
}
}
}
...@@ -9,26 +9,42 @@ import android.graphics.Rect; ...@@ -9,26 +9,42 @@ import android.graphics.Rect;
import android.view.View; import android.view.View;
import org.chromium.components.paintpreview.player.PlayerCompositorDelegate; import org.chromium.components.paintpreview.player.PlayerCompositorDelegate;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
/** /**
* Sets up the view and the logic behind it for a Paint Preview frame. * Sets up the view and the logic behind it for a Paint Preview frame.
*/ */
public class PlayerFrameCoordinator { public class PlayerFrameCoordinator {
private PlayerFrameMediator mMediator;
private PlayerFrameView mView;
/** /**
* Creates a {@link PlayerFrameMediator} and {@link PlayerFrameView} for this component and * Creates a {@link PlayerFrameMediator} and {@link PlayerFrameView} for this component and
* binds them together. * binds them together.
*/ */
public PlayerFrameCoordinator(Context context, PlayerCompositorDelegate compositorDelegate, public PlayerFrameCoordinator(Context context, PlayerCompositorDelegate compositorDelegate,
long frameGuid, int contentWidth, int contentHeight, boolean canDetectZoom) {} long frameGuid, int contentWidth, int contentHeight, boolean canDetectZoom) {
PropertyModel model = new PropertyModel.Builder(PlayerFrameProperties.ALL_KEYS).build();
mMediator = new PlayerFrameMediator(
model, compositorDelegate, frameGuid, contentWidth, contentHeight);
mView = new PlayerFrameView(context, canDetectZoom, mMediator);
PropertyModelChangeProcessor.create(model, mView, PlayerFrameViewBinder::bind);
}
/** /**
* Adds a child {@link PlayerFrameCoordinator} to this class. * Adds a child {@link PlayerFrameCoordinator} to this class.
* @param subFrame The sub-frame's {@link PlayerFrameCoordinator}. * @param subFrame The sub-frame's {@link PlayerFrameCoordinator}.
* @param clipRect The {@link Rect} in which this sub-frame should be shown in. * @param clipRect The {@link Rect} in which this sub-frame should be shown in.
*/ */
public void addSubFrame(PlayerFrameCoordinator subFrame, Rect clipRect) {} public void addSubFrame(PlayerFrameCoordinator subFrame, Rect clipRect) {
mMediator.addSubFrame(subFrame.getView(), clipRect);
}
/**
* @return The view associated with this component.
*/
public View getView() { public View getView() {
return null; return mView;
} }
} }
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.paintpreview.player.frame;
import android.content.Context;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
/**
* Detects scroll, fling, and scale gestures on calls to {@link #onTouchEvent} and reports back to
* the provided {@link PlayerFrameViewDelegate}.
*/
class PlayerFrameGestureDetector
implements GestureDetector.OnGestureListener, ScaleGestureDetector.OnScaleGestureListener {
private GestureDetector mGestureDetector;
private ScaleGestureDetector mScaleGestureDetector;
private boolean mCanDetectZoom;
private PlayerFrameViewDelegate mPlayerFrameViewDelegate;
/**
* @param context Used for initializing {@link GestureDetector} and
* {@link ScaleGestureDetector}.
* @param canDetectZoom Whether this {@link PlayerFrameGestureDetector} should detect scale
* gestures.
* @param playerFrameViewDelegate The delegate used when desired gestured are detected.
*/
PlayerFrameGestureDetector(Context context, boolean canDetectZoom,
PlayerFrameViewDelegate playerFrameViewDelegate) {
mGestureDetector = new GestureDetector(context, this);
mScaleGestureDetector = new ScaleGestureDetector(context, this);
mCanDetectZoom = canDetectZoom;
mPlayerFrameViewDelegate = playerFrameViewDelegate;
}
/**
* This should be called on every touch event.
* @return Whether the event was consumed.
*/
boolean onTouchEvent(MotionEvent event) {
if (mCanDetectZoom) {
mScaleGestureDetector.onTouchEvent(event);
}
return mGestureDetector.onTouchEvent(event);
}
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {}
@Override
public boolean onSingleTapUp(MotionEvent e) {
mPlayerFrameViewDelegate.onClick((int) e.getX(), (int) e.getY());
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return mPlayerFrameViewDelegate.scrollBy(distanceX, distanceY);
}
@Override
public void onLongPress(MotionEvent e) {}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
assert mCanDetectZoom;
return mPlayerFrameViewDelegate.scaleBy(
detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY());
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
assert mCanDetectZoom;
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.paintpreview.player.frame;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.Pair;
import android.view.View;
import org.chromium.base.Callback;
import org.chromium.components.paintpreview.player.PlayerCompositorDelegate;
import org.chromium.ui.modelutil.PropertyModel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Handles the business logic for the player frame component. Concretely, this class is responsible
* for:
* <ul>
* <li>Maintaining a viewport {@link Rect} that represents the current user-visible section of this
* frame. The dimension of the viewport is constant and is equal to the initial values received on
* {@link #setLayoutDimensions}.</li>
* <li>Constructing a matrix of {@link Bitmap} tiles that represents the content of this frame for a
* given scale factor. Each tile is as big as the view port.</li>
* <li>Requesting bitmaps from Paint Preview compositor.</li>
* <li>Updating the viewport on touch gesture notifications (scrolling and scaling).<li/>
* <li>Determining which sub-frames are visible given the current viewport and showing them.<li/>
* </ul>
*/
class PlayerFrameMediator implements PlayerFrameViewDelegate {
/** The GUID associated with the frame that this class is representing. */
private final long mGuid;
/** The content width inside this frame, at a scale factor of 1. */
private final int mContentWidth;
/** The content height inside this frame, at a scale factor of 1. */
private final int mContentHeight;
/**
* A list of {@link PlayerFrameCoordinator}s and {@link Rect}s representing this frame's
* sub-frames and their coordinates.
*/
private final List<Pair<View, Rect>> mSubFrames = new ArrayList<>();
private final PropertyModel mModel;
private final PlayerCompositorDelegate mCompositorDelegate;
/** The user-visible area for this frame. */
private final Rect mViewportRect = new Rect();
/** Rect used for requesting a new bitmap from Paint Preview compositor. */
private final Rect mBitmapRequestRect = new Rect();
/**
* A scale factor cache of matrices of bitmaps that make up the content of this frame at a
* given scale factor.
*/
private final Map<Float, Bitmap[][]> mBitmapMatrix = new HashMap<>();
/** Whether a request for a bitmap tile is pending, mapped by scale factor. */
private final Map<Float, boolean[][]> mPendingBitmapRequests = new HashMap<>();
/** The current scale factor. */
private float mScaleFactor;
PlayerFrameMediator(PropertyModel model, PlayerCompositorDelegate compositorDelegate,
long frameGuid, int contentWidth, int contentHeight) {
mModel = model;
mCompositorDelegate = compositorDelegate;
mGuid = frameGuid;
mContentWidth = contentWidth;
mContentHeight = contentHeight;
}
/**
* Adds a new sub-frame to this frame.
* @param subFrameView The {@link View} associated with the sub-frame.
* @param clipRect The bounds of the sub-frame, relative to this frame.
*/
void addSubFrame(View subFrameView, Rect clipRect) {
mSubFrames.add(new Pair<>(subFrameView, clipRect));
}
@Override
public void setLayoutDimensions(int width, int height) {
// If the dimensions of mViewportRect has been set, we don't need to do anything.
if (!mViewportRect.isEmpty() || width <= 0 || height <= 0) return;
// Set mViewportRect's dimensions and start compositing.
mViewportRect.set(0, 0, width, height);
updateViewport(0, 0, 1f);
}
/**
* Called when either the view port of the scale factor should be changed. Updates the view port
* and requests bitmap tiles for portion of the view port that don't have bitmap tiles.
* @param distanceX The horizontal distance that the view port should be moved by.
* @param distanceY The vertical distance that the view port should be moved by.
* @param scaleFactor The new scale factor.
*/
private void updateViewport(int distanceX, int distanceY, float scaleFactor) {
// TODO(crbug.com/1021111): Implement a caching mechanism that (i) fetches nearby tiles, and
// (ii) destroys bitmaps that are unlikely to be used soon.
// Initialize the bitmap matrix for this scale factor if we haven't already.
Bitmap[][] bitmapMatrix = mBitmapMatrix.get(scaleFactor);
boolean[][] pendingBitmapRequests = mPendingBitmapRequests.get(scaleFactor);
if (bitmapMatrix == null) {
// Each tile is as big as the view port. Here we determine the number of columns and
// rows for the current scale factor.
int rows = (int) Math.ceil((mContentHeight * scaleFactor) / mViewportRect.height());
int cols = (int) Math.ceil((mContentWidth * scaleFactor) / mViewportRect.width());
bitmapMatrix = new Bitmap[rows][cols];
mBitmapMatrix.put(scaleFactor, bitmapMatrix);
pendingBitmapRequests = new boolean[rows][cols];
mPendingBitmapRequests.put(scaleFactor, pendingBitmapRequests);
}
// If the scale factor is changed, the view should get the correct bitmap matrix.
if (scaleFactor != mScaleFactor) {
mModel.set(PlayerFrameProperties.BITMAP_MATRIX, mBitmapMatrix.get(scaleFactor));
mScaleFactor = scaleFactor;
}
// Update mViewportRect and let the view know. PropertyModelChangeProcessor is smart about
// this and will only update the view if mViewportRect is actually changed.
mViewportRect.offset(distanceX, distanceY);
mModel.set(PlayerFrameProperties.VIEWPORT, mViewportRect);
// Request bitmaps for tiles inside the view port that don't already have a bitmap.
final int tileWidth = mViewportRect.width();
final int tileHeight = mViewportRect.height();
final int colStart = mViewportRect.left / tileWidth;
final int colEnd = (int) Math.ceil((double) mViewportRect.right / tileWidth);
final int rowStart = mViewportRect.top / tileHeight;
final int rowEnd = (int) Math.ceil((double) mViewportRect.bottom / tileHeight);
for (int col = colStart; col < colEnd; col++) {
for (int row = rowStart; row < rowEnd; row++) {
if (bitmapMatrix[row][col] == null && !pendingBitmapRequests[row][col]) {
int tileLeft = col * tileWidth;
int tileTop = row * tileHeight;
mBitmapRequestRect.set(
tileLeft, tileTop, tileLeft + tileWidth, tileTop + tileHeight);
BitmapRequestHandler bitmapRequestHandler =
new BitmapRequestHandler(row, col, scaleFactor);
pendingBitmapRequests[row][col] = true;
mCompositorDelegate.requestBitmap(mGuid, mBitmapRequestRect, scaleFactor,
bitmapRequestHandler, bitmapRequestHandler);
}
}
}
// Add visible sub-frames to the view.
List<Pair<View, Rect>> visibleSubFrames = new ArrayList<>();
for (int i = 0; i < mSubFrames.size(); i++) {
// TODO(crbug.com/1020702): These values should be scaled for scale factors other than
// 1.
if (Rect.intersects(mSubFrames.get(i).second, mViewportRect)) {
visibleSubFrames.add(mSubFrames.get(i));
}
}
mModel.set(PlayerFrameProperties.SUBFRAME_VIEWS, visibleSubFrames);
}
/**
* Called on scroll events from the user. Checks if scrolling is possible, and if so, calls
* {@link #updateViewport}.
* @param distanceX Horizontal scroll distance in pixels.
* @param distanceY Vertical scroll distance in pixels.
* @return Whether the scrolling was possible and view port was updated.
*/
@Override
public boolean scrollBy(float distanceX, float distanceY) {
// TODO(crbug.com/1020702): These values should be scaled for scale factors other than 1.
int validDistanceX = 0;
int validDistanceY = 0;
if (mViewportRect.left > 0 && distanceX < 0) {
validDistanceX = (int) Math.max(distanceX, -1f * mViewportRect.left);
} else if (mViewportRect.right < mContentWidth && distanceX > 0) {
validDistanceX = (int) Math.min(distanceX, (float) mContentWidth - mViewportRect.right);
}
if (mViewportRect.top > 0 && distanceY < 0) {
validDistanceY = (int) Math.max(distanceY, -1f * mViewportRect.top);
} else if (mViewportRect.bottom < mContentHeight && distanceY > 0) {
validDistanceY =
(int) Math.min(distanceY, (float) mContentHeight - mViewportRect.bottom);
}
if (validDistanceX == 0 && validDistanceY == 0) return false;
updateViewport(validDistanceX, validDistanceY, mScaleFactor);
return true;
}
@Override
public boolean scaleBy(float scaleFactor, float focalPointX, float focalPointY) {
// TODO(crbug.com/1020702): Add support for zooming.
return false;
}
@Override
public void onClick(int x, int y) {
mCompositorDelegate.onClick(mGuid, new Point(x, y));
}
/**
* Used as the callback for bitmap requests from the Paint Preview compositor.
*/
private class BitmapRequestHandler implements Callback<Bitmap>, Runnable {
int mRequestRow;
int mRequestCol;
float mRequestScaleFactor;
private BitmapRequestHandler(int requestRow, int requestCol, float requestScaleFactor) {
mRequestRow = requestRow;
mRequestCol = requestCol;
mRequestScaleFactor = requestScaleFactor;
}
/**
* Called when bitmap is successfully composited.
* @param result
*/
@Override
public void onResult(Bitmap result) {
assert mBitmapMatrix.get(mRequestScaleFactor) != null;
assert mBitmapMatrix.get(mRequestScaleFactor)[mRequestRow][mRequestCol] == null;
assert mPendingBitmapRequests.get(mRequestScaleFactor)[mRequestRow][mRequestCol];
mBitmapMatrix.get(mScaleFactor)[mRequestRow][mRequestCol] = result;
mPendingBitmapRequests.get(mScaleFactor)[mRequestRow][mRequestCol] = false;
if (PlayerFrameMediator.this.mScaleFactor == mRequestScaleFactor) {
mModel.set(PlayerFrameProperties.BITMAP_MATRIX, mBitmapMatrix.get(mScaleFactor));
}
}
/**
* Called when there was an error compositing the bitmap.
*/
@Override
public void run() {
// TODO(crbug.com/1021590): Handle errors.
assert mBitmapMatrix.get(mRequestScaleFactor) != null;
assert mBitmapMatrix.get(mRequestScaleFactor)[mRequestRow][mRequestCol] == null;
assert mPendingBitmapRequests.get(mRequestScaleFactor)[mRequestRow][mRequestCol];
mPendingBitmapRequests.get(mScaleFactor)[mRequestRow][mRequestCol] = false;
}
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.paintpreview.player.frame;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.util.Pair;
import android.view.View;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
import java.util.List;
/**
* Contains all properties that a player frame {@link PropertyModel} can have.
*/
class PlayerFrameProperties {
/** A matrix of bitmap tiles that collectively make the entire content. */
static final PropertyModel.WritableObjectPropertyKey<Bitmap[][]> BITMAP_MATRIX =
new PropertyModel.WritableObjectPropertyKey<>();
/**
* Contains the current user-visible content window. The view should use this to draw the
* appropriate bitmap tiles from {@link #BITMAP_MATRIX}.
*/
static final PropertyModel.WritableObjectPropertyKey<Rect> VIEWPORT =
new PropertyModel.WritableObjectPropertyKey<>();
/**
* A list of sub-frames that are currently visible. Each element in the list is a {@link Pair}
* consists of a {@link View}, that displays the sub-frame's content, and a {@link Rect}, that
* contains its location.
*/
static final PropertyModel.WritableObjectPropertyKey<List<Pair<View, Rect>>> SUBFRAME_VIEWS =
new PropertyModel.WritableObjectPropertyKey<>();
static final PropertyKey[] ALL_KEYS = {BITMAP_MATRIX, VIEWPORT, SUBFRAME_VIEWS};
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.paintpreview.player.frame;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.util.Pair;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import java.util.List;
/**
* Responsible for detecting touch gestures, displaying the content of a frame and its sub-frames.
* {@link PlayerFrameBitmapPainter} is used for drawing the contents.
* Sub-frames are represented with individual {@link View}s. {@link #mSubFrames} contains the list
* of all sub-frames and their relative positions.
*/
class PlayerFrameView extends FrameLayout {
private PlayerFrameBitmapPainter mBitmapPainter;
private PlayerFrameGestureDetector mGestureDetector;
private PlayerFrameViewDelegate mDelegate;
private List<Pair<View, Rect>> mSubFrames;
/**
* @param context Used for initialization.
* @param canDetectZoom Whether this {@link View} should detect zoom (scale) gestures.
* @param playerFrameViewDelegate The interface used for forwarding events.
*/
PlayerFrameView(@NonNull Context context, boolean canDetectZoom,
PlayerFrameViewDelegate playerFrameViewDelegate) {
super(context);
mDelegate = playerFrameViewDelegate;
mBitmapPainter = new PlayerFrameBitmapPainter(this::invalidate);
mGestureDetector =
new PlayerFrameGestureDetector(context, canDetectZoom, playerFrameViewDelegate);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
mDelegate.setLayoutDimensions(getWidth(), getHeight());
for (int i = 0; i < mSubFrames.size(); i++) {
View childView = getChildAt(i);
if (childView == null) {
continue;
}
Rect childRect = mSubFrames.get(i).second;
childView.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
}
}
/**
* Updates the sub-frames that this {@link PlayerFrameView} should display, along with their
* coordinates.
* @param subFrames List of all sub-frames, along with their coordinates.
*/
void updateSubFrames(List<Pair<View, Rect>> subFrames) {
// TODO(mahmoudi): Removing all views every time is not smart. Only remove the views that
// are not in subFrames.first.
mSubFrames = subFrames;
removeAllViews();
for (int i = 0; i < subFrames.size(); i++) {
addView(subFrames.get(i).first, i);
}
}
void updateViewPort(int left, int top, int right, int bottom) {
mBitmapPainter.updateViewPort(left, top, right, bottom);
}
void updateBitmapMatrix(Bitmap[][] bitmapMatrix) {
mBitmapPainter.updateBitmapMatrix(bitmapMatrix);
}
@Override
protected void onDraw(Canvas canvas) {
mBitmapPainter.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return mGestureDetector.onTouchEvent(event) || super.onTouchEvent(event);
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.paintpreview.player.frame;
import android.graphics.Rect;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
/**
* Binds property changes in {@link PropertyModel} to {@link PlayerFrameView}.
*/
class PlayerFrameViewBinder {
static void bind(PropertyModel model, PlayerFrameView view, PropertyKey key) {
if (key.equals(PlayerFrameProperties.BITMAP_MATRIX)) {
view.updateBitmapMatrix(model.get(PlayerFrameProperties.BITMAP_MATRIX));
} else if (key.equals(PlayerFrameProperties.VIEWPORT)) {
Rect viewPort = model.get(PlayerFrameProperties.VIEWPORT);
view.updateViewPort(viewPort.left, viewPort.top, viewPort.right, viewPort.bottom);
} else if (key.equals(PlayerFrameProperties.SUBFRAME_VIEWS)) {
view.updateSubFrames(model.get(PlayerFrameProperties.SUBFRAME_VIEWS));
}
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.paintpreview.player.frame;
/**
* Used by {@link PlayerFrameView} to delegate view events to {@link PlayerFrameMediator}.
*/
interface PlayerFrameViewDelegate {
/**
* Called on layout with the attributed width and height.
*/
void setLayoutDimensions(int width, int height);
/**
* Called when a scroll gesture is performed.
* @param distanceX Horizontal scroll values in pixels.
* @param distanceY Vertical scroll values in pixels.
* @return Whether this scroll event was consumed.
*/
boolean scrollBy(float distanceX, float distanceY);
/**
* Called when a scale gesture is performed.
* @return Whether this scale event was consumed.
*/
boolean scaleBy(float scaleFactor, float focalPointX, float focalPointY);
/**
* Called when a single tap gesture is performed.
* @param x X coordinate of the point clicked.
* @param y Y coordinate of the point clicked.
*/
void onClick(int x, int y);
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.paintpreview.player.frame;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.chromium.base.test.BaseRobolectricTestRunner;
import java.util.ArrayList;
import java.util.List;
/**
* Tests for the {@link PlayerFrameBitmapPainter} class.
*/
@RunWith(BaseRobolectricTestRunner.class)
public class PlayerFrameBitmapPainterTest {
/**
* Mocks {@link Canvas} and holds all calls to
* {@link Canvas#drawBitmap(Bitmap, Rect, Rect, Paint)}.
*/
private class MockCanvas extends Canvas {
private List<DrawnBitmap> mDrawnBitmaps = new ArrayList<>();
private class DrawnBitmap {
private final Bitmap mBitmap;
private final Rect mSrc;
private final Rect mDst;
private DrawnBitmap(Bitmap bitmap, Rect src, Rect dst) {
mBitmap = bitmap;
mSrc = new Rect(src);
mDst = new Rect(dst);
}
@Override
public boolean equals(Object o) {
if (o == null) return false;
if (this == o) return true;
if (getClass() != o.getClass()) return false;
DrawnBitmap od = (DrawnBitmap) o;
return mBitmap.equals(od.mBitmap) && mSrc.equals(od.mSrc) && mDst.equals(od.mDst);
}
}
@Override
public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst,
@Nullable Paint paint) {
mDrawnBitmaps.add(new DrawnBitmap(bitmap, src, dst));
}
/**
* Asserts if a portion of a given bitmap has been drawn on this canvas.
*/
private void assertDrawBitmap(
@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst) {
Assert.assertTrue(bitmap + " has not been drawn from " + src + " to " + dst,
mDrawnBitmaps.contains(new DrawnBitmap(bitmap, src, dst)));
}
/**
* Asserts the number of bitmap draw operations on this canvas.
*/
private void assertNumberOfBitmapDraws(int expected) {
Assert.assertEquals(expected, mDrawnBitmaps.size());
}
}
/**
* Verifies no draw operations are performed on the canvas if the view port is invalid.
*/
@Test
public void testDrawFaultyViewPort() {
PlayerFrameBitmapPainter painter =
new PlayerFrameBitmapPainter(Mockito.mock(Runnable.class));
painter.updateBitmapMatrix(new Bitmap[2][3]);
painter.updateViewPort(0, 5, 10, -10);
MockCanvas canvas = new MockCanvas();
painter.onDraw(canvas);
canvas.assertNumberOfBitmapDraws(0);
// Update the view port so it is covered by 2 bitmap tiles.
painter.updateViewPort(0, 5, 10, 15);
painter.onDraw(canvas);
canvas.assertNumberOfBitmapDraws(2);
}
/**
* Verifies no draw operations are performed on the canvas if the bitmap matrix is invalid.
*/
@Test
public void testDrawFaultyBitmapMatrix() {
PlayerFrameBitmapPainter painter =
new PlayerFrameBitmapPainter(Mockito.mock(Runnable.class));
painter.updateBitmapMatrix(new Bitmap[0][0]);
// This view port is covered by 2 bitmap tiles, so there should be 2 draw operations on
// the canvas.
painter.updateViewPort(0, 5, 10, 15);
MockCanvas canvas = new MockCanvas();
painter.onDraw(canvas);
canvas.assertNumberOfBitmapDraws(0);
painter.updateBitmapMatrix(new Bitmap[2][1]);
painter.onDraw(canvas);
canvas.assertNumberOfBitmapDraws(2);
}
/**
* Verified {@link PlayerFrameBitmapPainter#onDraw} draws the right bitmap tiles, at the correct
* coordinates, for the given view port.
*/
@Test
public void testDraw() {
Runnable invalidator = Mockito.mock(Runnable.class);
PlayerFrameBitmapPainter painter = new PlayerFrameBitmapPainter(invalidator);
// Prepare the bitmap matrix.
Bitmap[][] bitmaps = new Bitmap[2][2];
Bitmap bitmap00 = Mockito.mock(Bitmap.class);
Bitmap bitmap10 = Mockito.mock(Bitmap.class);
Bitmap bitmap01 = Mockito.mock(Bitmap.class);
Bitmap bitmap11 = Mockito.mock(Bitmap.class);
bitmaps[0][0] = bitmap00;
bitmaps[1][0] = bitmap10;
bitmaps[0][1] = bitmap01;
bitmaps[1][1] = bitmap11;
painter.updateBitmapMatrix(bitmaps);
painter.updateViewPort(5, 10, 15, 25);
// Make sure the invalidator was called after updating the bitmap matrix and the view port.
Mockito.verify(invalidator, Mockito.times(2)).run();
MockCanvas canvas = new MockCanvas();
painter.onDraw(canvas);
// Verify that the correct portions of each bitmap tiles is painted in the correct
// positions of in the canvas.
canvas.assertNumberOfBitmapDraws(4);
canvas.assertDrawBitmap(bitmap00, new Rect(5, 10, 10, 15), new Rect(0, 0, 5, 5));
canvas.assertDrawBitmap(bitmap10, new Rect(5, 0, 10, 10), new Rect(0, 5, 5, 15));
canvas.assertDrawBitmap(bitmap01, new Rect(0, 10, 5, 15), new Rect(5, 0, 10, 5));
canvas.assertDrawBitmap(bitmap11, new Rect(0, 0, 5, 10), new Rect(5, 5, 10, 15));
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.paintpreview.player.frame;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.Pair;
import android.view.View;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.chromium.base.Callback;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.components.paintpreview.player.PlayerCompositorDelegate;
import org.chromium.ui.modelutil.PropertyModel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Tests for the {@link PlayerFrameMediator} class.
*/
@RunWith(BaseRobolectricTestRunner.class)
public class PlayerFrameMediatorTest {
private static final long FRAME_GUID = 123321L;
private static final int CONTENT_WIDTH = 560;
private static final int CONTENT_HEIGHT = 1150;
private PropertyModel mModel;
private TestPlayerCompositorDelegate mCompositorDelegate;
private PlayerFrameMediator mMediator;
/**
* Used for keeping track of all bitmap requests that {@link PlayerFrameMediator} makes.
*/
private class RequestedBitmap {
long mFrameGuid;
Rect mClipRect;
float mScaleFactor;
Callback<Bitmap> mBitmapCallback;
Runnable mErrorCallback;
public RequestedBitmap(long frameGuid, Rect clipRect, float scaleFactor,
Callback<Bitmap> bitmapCallback, Runnable errorCallback) {
this.mFrameGuid = frameGuid;
this.mClipRect = clipRect;
this.mScaleFactor = scaleFactor;
this.mBitmapCallback = bitmapCallback;
this.mErrorCallback = errorCallback;
}
public RequestedBitmap(long frameGuid, Rect clipRect, float scaleFactor) {
this.mFrameGuid = frameGuid;
this.mClipRect = clipRect;
this.mScaleFactor = scaleFactor;
}
@Override
public boolean equals(Object o) {
if (o == null) return false;
if (o == this) return true;
if (o.getClass() != this.getClass()) return false;
RequestedBitmap rb = (RequestedBitmap) o;
return rb.mClipRect.equals(mClipRect) && rb.mFrameGuid == mFrameGuid
&& rb.mScaleFactor == mScaleFactor;
}
@Override
public String toString() {
return mFrameGuid + ", " + mClipRect + ", " + mScaleFactor;
}
}
/**
* Mocks {@link PlayerCompositorDelegate}. Stores all bitmap requests as
* {@link RequestedBitmap}s.
*/
private class TestPlayerCompositorDelegate implements PlayerCompositorDelegate {
List<RequestedBitmap> mRequestedBitmap = new ArrayList<>();
@Override
public void requestBitmap(long frameGuid, Rect clipRect, float scaleFactor,
Callback<Bitmap> bitmapCallback, Runnable errorCallback) {
mRequestedBitmap.add(new RequestedBitmap(
frameGuid, new Rect(clipRect), scaleFactor, bitmapCallback, errorCallback));
}
@Override
public void onClick(long frameGuid, Point point) {}
}
@Before
public void setUp() {
mModel = new PropertyModel.Builder(PlayerFrameProperties.ALL_KEYS).build();
mCompositorDelegate = new TestPlayerCompositorDelegate();
mMediator = new PlayerFrameMediator(
mModel, mCompositorDelegate, FRAME_GUID, CONTENT_WIDTH, CONTENT_HEIGHT);
}
/**
* Tests that {@link PlayerFrameMediator} is initialized correctly on the first call to
* {@link PlayerFrameMediator#setLayoutDimensions}.
*/
@Test
public void testInitialLayoutDimensions() {
// Initial view port setup.
mMediator.setLayoutDimensions(150, 200);
// View port should be as big as size set in the first setLayoutDimensions call, showing
// the top left corner.
Rect expectedViewPort = new Rect(0, 0, 150, 200);
Assert.assertEquals(expectedViewPort, mModel.get(PlayerFrameProperties.VIEWPORT));
// The bitmap matrix should be empty, but initialized with the correct number of rows and
// columns.
Bitmap[][] bitmapMatrix = mModel.get(PlayerFrameProperties.BITMAP_MATRIX);
Assert.assertTrue(Arrays.deepEquals(bitmapMatrix, new Bitmap[6][4]));
Assert.assertEquals(new ArrayList<Pair<View, Rect>>(),
mModel.get(PlayerFrameProperties.SUBFRAME_VIEWS));
}
/**
* Tests that {@link PlayerFrameMediator} requests for the right bitmap tiles as the view port
* moves.
*/
@Test
public void testBitmapRequest() {
// Initial view port setup.
mMediator.setLayoutDimensions(100, 200);
// Requests for bitmaps in all tiles that are visible in the view port should've been made.
// Since the current view port fully matches the top left bitmap tile, that should be the
// only requested bitmap.
List<RequestedBitmap> expectedRequestedBitmaps = new ArrayList<>();
expectedRequestedBitmaps.add(new RequestedBitmap(FRAME_GUID, new Rect(0, 0, 100, 200), 1f));
Assert.assertEquals(expectedRequestedBitmaps, mCompositorDelegate.mRequestedBitmap);
mMediator.scrollBy(10, 20);
// The view port was moved with the #updateViewport call. It should've been updated in the
// model.
Rect expectedViewPort = new Rect(10, 20, 110, 220);
Assert.assertEquals(expectedViewPort, mModel.get(PlayerFrameProperties.VIEWPORT));
// The current viewport covers portions of 4 adjacent bitmap tiles. Make sure requests for
// compositing those bitmap tiles are made.
expectedRequestedBitmaps.add(
new RequestedBitmap(FRAME_GUID, new Rect(0, 200, 100, 400), 1f));
expectedRequestedBitmaps.add(
new RequestedBitmap(FRAME_GUID, new Rect(100, 0, 200, 200), 1f));
expectedRequestedBitmaps.add(
new RequestedBitmap(FRAME_GUID, new Rect(100, 200, 200, 400), 1f));
Assert.assertEquals(expectedRequestedBitmaps, mCompositorDelegate.mRequestedBitmap);
// Move the view port slightly. It is still covered by the same 4 tiles. Since there were
// already bitmap requests out for those tiles, we shouldn't have made new requests.
mMediator.scrollBy(10, 20);
Assert.assertEquals(expectedRequestedBitmaps, mCompositorDelegate.mRequestedBitmap);
// Move the view port to the bottom right so it covers portions of the 4 bottom right bitmap
// tiles. 4 new bitmap requests should be made.
mMediator.scrollBy(430, 900);
expectedViewPort.set(450, 940, 550, 1140);
Assert.assertEquals(expectedViewPort, mModel.get(PlayerFrameProperties.VIEWPORT));
expectedRequestedBitmaps.add(
new RequestedBitmap(FRAME_GUID, new Rect(400, 800, 500, 1000), 1f));
expectedRequestedBitmaps.add(
new RequestedBitmap(FRAME_GUID, new Rect(400, 1000, 500, 1200), 1f));
expectedRequestedBitmaps.add(
new RequestedBitmap(FRAME_GUID, new Rect(500, 800, 600, 1000), 1f));
expectedRequestedBitmaps.add(
new RequestedBitmap(FRAME_GUID, new Rect(500, 1000, 600, 1200), 1f));
Assert.assertEquals(expectedRequestedBitmaps, mCompositorDelegate.mRequestedBitmap);
}
/**
* Mocks responses on bitmap requests from {@link PlayerFrameMediator} and tests those responses
* are correctly handled.
*/
@Test
public void testBitmapRequestResponse() {
// Sets the bitmap tile size to 100x200 and triggers bitmap request for the upper left tile.
mMediator.setLayoutDimensions(150, 200);
// Create mock bitmap for response.
Bitmap bitmap1 = Mockito.mock(Bitmap.class);
Bitmap[][] expectedBitmapMatrix = new Bitmap[6][4];
expectedBitmapMatrix[0][0] = bitmap1;
// Call the request callback with the mock bitmap and assert it's added to the model.
mCompositorDelegate.mRequestedBitmap.get(0).mBitmapCallback.onResult(bitmap1);
Assert.assertTrue(Arrays.deepEquals(
expectedBitmapMatrix, mModel.get(PlayerFrameProperties.BITMAP_MATRIX)));
Assert.assertEquals(new ArrayList<Pair<View, Rect>>(),
mModel.get(PlayerFrameProperties.SUBFRAME_VIEWS));
// Move the viewport to an area that is covered by 3 additional tiles. Triggers bitmap
// requests for an additional 3 tiles.
mMediator.scrollBy(10, 10);
// Assert that there are the only 4 total bitmap requests, i.e. we didn't request for the
// tile at [0][0] again.
List<RequestedBitmap> expectedRequestedBitmaps = new ArrayList<>();
expectedRequestedBitmaps.add(new RequestedBitmap(FRAME_GUID, new Rect(0, 0, 150, 200), 1f));
expectedRequestedBitmaps.add(
new RequestedBitmap(FRAME_GUID, new Rect(0, 200, 150, 400), 1f));
expectedRequestedBitmaps.add(
new RequestedBitmap(FRAME_GUID, new Rect(150, 0, 300, 200), 1f));
expectedRequestedBitmaps.add(
new RequestedBitmap(FRAME_GUID, new Rect(150, 200, 300, 400), 1f));
Assert.assertEquals(expectedRequestedBitmaps, mCompositorDelegate.mRequestedBitmap);
// Mock a compositing failure for the second request.
mCompositorDelegate.mRequestedBitmap.get(1).mErrorCallback.run();
// Move the view port while staying within the 4 bitmap tiles in order to trigger the
// request logic again. Make sure only a new request is added, for the tile with a
// compositing failure.
mMediator.scrollBy(10, 10);
expectedRequestedBitmaps.add(
new RequestedBitmap(FRAME_GUID, new Rect(0, 200, 150, 400), 1f));
Assert.assertEquals(expectedRequestedBitmaps, mCompositorDelegate.mRequestedBitmap);
}
/**
* View port should be updated on scroll events, but it shouldn't go out of content bounds.
*/
@Test
public void testViewPortOnScrollBy() {
// Initial view port setup.
mMediator.setLayoutDimensions(100, 200);
Rect expectedViewPort = new Rect(0, 0, 100, 200);
// Scroll right and down by a within bounds amount. Both scroll directions should be
// effective.
Assert.assertTrue(mMediator.scrollBy(250f, 80f));
expectedViewPort.offset(250, 80);
Assert.assertEquals(expectedViewPort, mModel.get(PlayerFrameProperties.VIEWPORT));
// Scroll by an out of bounds horizontal value. Should be scrolled to the rightmost point.
Assert.assertTrue(mMediator.scrollBy(1000f, 50f));
expectedViewPort.offset(210, 50);
Assert.assertEquals(expectedViewPort, mModel.get(PlayerFrameProperties.VIEWPORT));
// Scroll by an out of bounds horizontal and vertical value.
// Should be scrolled to the bottom right point.
Assert.assertTrue(mMediator.scrollBy(600f, 5000f));
expectedViewPort.offset(0, 820);
Assert.assertEquals(expectedViewPort, mModel.get(PlayerFrameProperties.VIEWPORT));
// Scroll right and down. Should be impossible since we're already at the bottom right
// point.
Assert.assertFalse(mMediator.scrollBy(10f, 15f));
expectedViewPort.offset(0, 0);
Assert.assertEquals(expectedViewPort, mModel.get(PlayerFrameProperties.VIEWPORT));
// Scroll right and up. Horizontal scroll should be ignored and should be scrolled to the
// top.
Assert.assertTrue(mMediator.scrollBy(100f, -2000f));
expectedViewPort.offset(0, -950);
Assert.assertEquals(expectedViewPort, mModel.get(PlayerFrameProperties.VIEWPORT));
// Scroll right and up. Both scroll directions should be ignored.
Assert.assertFalse(mMediator.scrollBy(100f, -2000f));
expectedViewPort.offset(0, 0);
Assert.assertEquals(expectedViewPort, mModel.get(PlayerFrameProperties.VIEWPORT));
// Scroll left and up. Vertical scroll should be ignored and should be scrolled to the
// left.
Assert.assertTrue(mMediator.scrollBy(-1000f, -2000f));
expectedViewPort.offset(-460, 0);
Assert.assertEquals(expectedViewPort, mModel.get(PlayerFrameProperties.VIEWPORT));
// Scroll left and up. Both scroll directions should be ignored.
Assert.assertFalse(mMediator.scrollBy(-1000f, -2000f));
expectedViewPort.offset(0, 0);
Assert.assertEquals(expectedViewPort, mModel.get(PlayerFrameProperties.VIEWPORT));
// Scroll left and down. Horizontal scroll should be ignored and should be scrolled to the
// bottom.
Assert.assertTrue(mMediator.scrollBy(-1000f, 2000f));
expectedViewPort.offset(0, 950);
Assert.assertEquals(expectedViewPort, mModel.get(PlayerFrameProperties.VIEWPORT));
// Scroll left and down. Both scroll directions should be ignored.
Assert.assertFalse(mMediator.scrollBy(-1000f, 2000f));
expectedViewPort.offset(0, 0);
Assert.assertEquals(expectedViewPort, mModel.get(PlayerFrameProperties.VIEWPORT));
// Scroll right and up. Both scroll values should be reflected.
Assert.assertTrue(mMediator.scrollBy(200, -100));
expectedViewPort.offset(200, -100);
Assert.assertEquals(expectedViewPort, mModel.get(PlayerFrameProperties.VIEWPORT));
}
/**
* Tests sub-frames' visibility when view port changes. sub-frames that are out of the view
* port's bounds should not be added to the model.
*/
@Test
public void testSubFramesPosition() {
Pair<View, Rect> subFrame1 =
new Pair<>(Mockito.mock(View.class), new Rect(10, 20, 60, 120));
Pair<View, Rect> subFrame2 =
new Pair<>(Mockito.mock(View.class), new Rect(30, 130, 70, 160));
Pair<View, Rect> subFrame3 =
new Pair<>(Mockito.mock(View.class), new Rect(120, 35, 150, 65));
mMediator.addSubFrame(subFrame1.first, subFrame1.second);
mMediator.addSubFrame(subFrame2.first, subFrame2.second);
mMediator.addSubFrame(subFrame3.first, subFrame3.second);
// Initial view port setup.
mMediator.setLayoutDimensions(100, 200);
List<Pair<View, Rect>> expectedVisibleViews = new ArrayList<>();
expectedVisibleViews.add(subFrame1);
expectedVisibleViews.add(subFrame2);
Assert.assertEquals(expectedVisibleViews, mModel.get(PlayerFrameProperties.SUBFRAME_VIEWS));
mMediator.scrollBy(100, 0);
expectedVisibleViews.clear();
expectedVisibleViews.add(subFrame3);
Assert.assertEquals(expectedVisibleViews, mModel.get(PlayerFrameProperties.SUBFRAME_VIEWS));
mMediator.scrollBy(-50, 0);
expectedVisibleViews.clear();
expectedVisibleViews.add(subFrame1);
expectedVisibleViews.add(subFrame2);
expectedVisibleViews.add(subFrame3);
Assert.assertEquals(expectedVisibleViews, mModel.get(PlayerFrameProperties.SUBFRAME_VIEWS));
mMediator.scrollBy(0, 200);
expectedVisibleViews.clear();
Assert.assertEquals(expectedVisibleViews, mModel.get(PlayerFrameProperties.SUBFRAME_VIEWS));
}
/**
* TODO(crbug.com/1020702): Implement after zooming support is added.
*/
@Test
public void testViewPortOnScaleBy() {}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment