Bug 663803 - Zoomed view implementation using render document r=mcomella,snorp

This commit is contained in:
dominique vincent 2015-01-21 05:59:23 +01:00
parent d15b47438e
commit 67724cb7a7
18 changed files with 835 additions and 20 deletions

View File

@ -955,7 +955,7 @@ public class BrowserApp extends GeckoApp
if (enabled) {
if (mLayerView != null) {
mLayerView.setOnMetricsChangedListener(this);
mLayerView.setOnMetricsChangedDynamicToolbarViewportListener(this);
}
setToolbarMargin(0);
mHomePagerContainer.setPadding(0, mBrowserChrome.getHeight(), 0, 0);
@ -963,7 +963,7 @@ public class BrowserApp extends GeckoApp
// Immediately show the toolbar when disabling the dynamic
// toolbar.
if (mLayerView != null) {
mLayerView.setOnMetricsChangedListener(null);
mLayerView.setOnMetricsChangedDynamicToolbarViewportListener(null);
}
mHomePagerContainer.setPadding(0, 0, 0, 0);
if (mBrowserChrome != null) {

View File

@ -105,7 +105,8 @@ public class GeckoEvent {
TELEMETRY_UI_EVENT(44),
GAMEPAD_ADDREMOVE(45),
GAMEPAD_DATA(46),
LONG_PRESS(47);
LONG_PRESS(47),
ZOOMEDVIEW(48);
public final int value;
@ -749,6 +750,17 @@ public class GeckoEvent {
return event;
}
public static GeckoEvent createZoomedViewEvent(int tabId, int x, int y, int bufw, int bufh, float scaleFactor, ByteBuffer buffer) {
GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.ZOOMEDVIEW);
event.mPoints = new Point[2];
event.mPoints[0] = new Point(x, y);
event.mPoints[1] = new Point(bufw, bufh);
event.mX = (double) scaleFactor;
event.mMetaState = tabId;
event.mBuffer = buffer;
return event;
}
public static GeckoEvent createScreenOrientationEvent(short aScreenOrientation) {
GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.SCREENORIENTATION_CHANGED);
event.mScreenOrientation = aScreenOrientation;

View File

@ -0,0 +1,490 @@
package org.mozilla.gecko;
import java.text.DecimalFormat;
import java.nio.ByteBuffer;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
import org.mozilla.gecko.gfx.LayerView;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.mozglue.DirectBufferAllocator;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.BitmapFactory;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.RelativeLayout;
public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChangedListener,
LayerView.OnZoomedViewListener, GeckoEventListener {
private static final String LOGTAG = "Gecko" + ZoomedView.class.getSimpleName();
private static final int ZOOM_FACTOR = 2;
private static final int W_CAPTURED_VIEW_IN_PERCENT = 80;
private static final int H_CAPTURED_VIEW_IN_PERCENT = 50;
private static final int MINIMUM_DELAY_BETWEEN_TWO_RENDER_CALLS_NS = 1000000;
private static final int DELAY_BEFORE_NEXT_RENDER_REQUEST_MS = 2000;
private ImageView zoomedImageView;
private LayerView layerView;
private MotionEvent actionDownEvent;
private int viewWidth;
private int viewHeight;
private int xLastPosition;
private int yLastPosition;
private boolean shouldSetVisibleOnUpdate;
private PointF convertedPosition;
private PointF returnValue;
private boolean stopUpdateView;
private int lastOrientation = 0;
private ByteBuffer buffer;
private Runnable requestRenderRunnable;
private long startTimeReRender = 0;
private long lastStartTimeReRender = 0;
private class ZoomedViewTouchListener implements View.OnTouchListener {
private float originRawX;
private float originRawY;
private int touchState;
@Override
public boolean onTouch(View view, MotionEvent event) {
if (layerView == null) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
if (moveZoomedView(event)) {
touchState = MotionEvent.ACTION_MOVE;
}
break;
case MotionEvent.ACTION_UP:
if (touchState == MotionEvent.ACTION_MOVE) {
touchState = -1;
} else {
layerView.dispatchTouchEvent(actionDownEvent);
actionDownEvent.recycle();
convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY());
MotionEvent e = MotionEvent.obtain(event.getDownTime(), event.getEventTime(),
MotionEvent.ACTION_UP, convertedPosition.x, convertedPosition.y,
event.getMetaState());
layerView.dispatchTouchEvent(e);
e.recycle();
}
break;
case MotionEvent.ACTION_DOWN:
touchState = -1;
originRawX = event.getRawX();
originRawY = event.getRawY();
convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY());
actionDownEvent = MotionEvent.obtain(event.getDownTime(), event.getEventTime(),
MotionEvent.ACTION_DOWN, convertedPosition.x, convertedPosition.y,
event.getMetaState());
break;
}
return true;
}
private boolean moveZoomedView(MotionEvent event) {
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) ZoomedView.this.getLayoutParams();
if ((touchState != MotionEvent.ACTION_MOVE) && (Math.abs((int) (event.getRawX() - originRawX)) < 1)
&& (Math.abs((int) (event.getRawY() - originRawY)) < 1)) {
// When the user just touches the screen ACTION_MOVE can be detected for a very small delta on position.
// In this case, the move is ignored if the delta is lower than 1 unit.
return false;
}
float newLeftMargin = params.leftMargin + event.getRawX() - originRawX;
float newTopMargin = params.topMargin + event.getRawY() - originRawY;
ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
ZoomedView.this.moveZoomedView(metrics, newLeftMargin, newTopMargin);
originRawX = event.getRawX();
originRawY = event.getRawY();
return true;
}
}
public ZoomedView(Context context) {
this(context, null, 0);
}
public ZoomedView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ZoomedView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
convertedPosition = new PointF();
returnValue = new PointF();
requestRenderRunnable = new Runnable() {
@Override
public void run() {
requestZoomedViewRender();
}
};
EventDispatcher.getInstance().registerGeckoThreadListener(this, "Gesture:nothingDoneOnLongPress",
"Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange");
}
void destroy() {
ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable);
EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Gesture:nothingDoneOnLongPress",
"Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange");
}
// This method (onFinishInflate) is called only when the zoomed view class is used inside
// an xml structure <org.mozilla.gecko.ZoomedView ...
// It won't be called if the class is used from java code like "new ZoomedView(context);"
@Override
protected void onFinishInflate() {
super.onFinishInflate();
ImageView closeButton = (ImageView) findViewById(R.id.dialog_close);
closeButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
stopZoomDisplay();
}
});
zoomedImageView = (ImageView) findViewById(R.id.zoomed_image_view);
zoomedImageView.setOnTouchListener(new ZoomedViewTouchListener());
}
/*
* Convert a click from ZoomedView. Return the position of the click in the
* LayerView
*/
private PointF getUnzoomedPositionFromPointInZoomedView(float x, float y) {
ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
PointF offset = metrics.getMarginOffset();
final float parentWidth = metrics.getWidth();
final float parentHeight = metrics.getHeight();
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
returnValue.x = (int) ((x / ZOOM_FACTOR) + // Conversion of the x offset inside the zoomed view (using the scale factor)
offset.x + // The offset of the layerView
/* Conversion of the left side position of the zoomed view
* Minimum value for the left side of the zoomed view is 0
* and we return 0 after conversion
* Maximum value for the left side of the zoomed view is (parentWidth - offset.x - viewWidth)
* and we return (parentWidth - offset.x - (viewWidth / ZOOM_FACTOR)) after conversion.
*/
(((float) params.leftMargin) - offset.x) *
((parentWidth - offset.x - (viewWidth / ZOOM_FACTOR)) /
(parentWidth - offset.x - viewWidth)));
// Same comments here vertically
returnValue.y = (int) ((y / ZOOM_FACTOR) +
offset.y +
(((float) params.topMargin) - offset.y) *
((parentHeight - offset.y - (viewHeight / ZOOM_FACTOR)) /
(parentHeight - offset.y - viewHeight)));
return returnValue;
}
/*
* A touch point (x,y) occurs in LayerView, this point should be displayed
* in the center of the zoomed view. The returned point is the position of
* the Top-Left zoomed view point on the screen device
*/
private PointF getZoomedViewTopLeftPositionFromTouchPosition(float x, float y) {
ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
PointF offset = metrics.getMarginOffset();
final float parentWidth = metrics.getWidth();
final float parentHeight = metrics.getHeight();
returnValue.x = (int) ((((x - (viewWidth / (2 * ZOOM_FACTOR)))) / // Translation to get the left side position of the zoomed view
// centered on x (the value 2 to get the middle).
/* Conversion of the left side position of the zoomed view.
* See the comment in getUnzoomedPositionFromPointInZoomedView.
* The proportional factor is the same. It is used in a division
* and not in a multiplication to convert the position from
* the LayerView to the ZoomedView.
*/
((parentWidth - offset.x - (viewWidth / ZOOM_FACTOR)) /
(parentWidth - offset.x - viewWidth)))
+ offset.x); // The offset of the layerView
// Same comments here vertically
returnValue.y = (int) ((((y - (viewHeight / (2 * ZOOM_FACTOR)))) /
((parentHeight - offset.y - (viewHeight / ZOOM_FACTOR)) /
(parentHeight - offset.y - viewHeight)))
+ offset.y);
return returnValue;
}
private void moveZoomedView(ImmutableViewportMetrics metrics, float newLeftMargin, float newTopMargin) {
if (layerView == null) {
return;
}
final float parentWidth = metrics.getWidth();
final float parentHeight = metrics.getHeight();
RelativeLayout.LayoutParams newLayoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
newLayoutParams.leftMargin = (int) newLeftMargin;
newLayoutParams.topMargin = (int) newTopMargin;
int topMarginMin;
int leftMarginMin;
PointF offset = metrics.getMarginOffset();
topMarginMin = (int) offset.y;
leftMarginMin = (int) offset.x;
if (newTopMargin < topMarginMin) {
newLayoutParams.topMargin = topMarginMin;
} else if (newTopMargin + viewHeight >= parentHeight) {
newLayoutParams.topMargin = (int) (parentHeight - viewHeight);
}
if (newLeftMargin < leftMarginMin) {
newLayoutParams.leftMargin = leftMarginMin;
} else if (newLeftMargin + viewWidth > parentWidth) {
newLayoutParams.leftMargin = (int) (parentWidth - viewWidth);
}
setLayoutParams(newLayoutParams);
convertedPosition = getUnzoomedPositionFromPointInZoomedView(0, 0);
xLastPosition = Math.round(convertedPosition.x);
yLastPosition = Math.round(convertedPosition.y);
requestZoomedViewRender();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// In case of orientation change, the zoomed view update is stopped until the orientation change
// is completed. At this time, the function onMetricsChanged is called and the
// zoomed view update is restarted again.
if (lastOrientation != newConfig.orientation) {
shouldBlockUpdate(true);
lastOrientation = newConfig.orientation;
}
}
public void refreshZoomedViewSize(ImmutableViewportMetrics viewport) {
if (layerView == null) {
return;
}
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
setCapturedSize(viewport);
moveZoomedView(viewport, params.leftMargin, params.topMargin);
}
public void setCapturedSize(ImmutableViewportMetrics metrics) {
if (layerView == null) {
return;
}
float parentMinSize = Math.min(metrics.getWidth(), metrics.getHeight());
viewWidth = (int) (parentMinSize * W_CAPTURED_VIEW_IN_PERCENT / (ZOOM_FACTOR * 100.0)) * ZOOM_FACTOR;
viewHeight = (int) (parentMinSize * H_CAPTURED_VIEW_IN_PERCENT / (ZOOM_FACTOR * 100.0)) * ZOOM_FACTOR;
}
public void shouldBlockUpdate(boolean shouldBlockUpdate) {
stopUpdateView = shouldBlockUpdate;
}
public Bitmap.Config getBitmapConfig() {
return (GeckoAppShell.getScreenDepth() == 24) ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
}
public void startZoomDisplay(LayerView aLayerView, final int leftFromGecko, final int topFromGecko) {
if (layerView == null) {
layerView = aLayerView;
layerView.addOnZoomedViewListener(this);
layerView.setOnMetricsChangedZoomedViewportListener(this);
ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
setCapturedSize(metrics);
}
startTimeReRender = 0;
shouldSetVisibleOnUpdate = true;
moveUsingGeckoPosition(leftFromGecko, topFromGecko);
}
public void stopZoomDisplay() {
shouldSetVisibleOnUpdate = false;
this.setVisibility(View.GONE);
ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable);
if (layerView != null) {
layerView.setOnMetricsChangedZoomedViewportListener(null);
layerView.removeOnZoomedViewListener(this);
layerView = null;
}
}
@Override
public void handleMessage(final String event, final JSONObject message) {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
try {
if (event.equals("Gesture:nothingDoneOnLongPress") || event.equals("Gesture:clusteredLinksClicked")) {
final JSONObject clickPosition = message.getJSONObject("clickPosition");
int left = clickPosition.getInt("x");
int top = clickPosition.getInt("y");
// Start to display inside the zoomedView
LayerView geckoAppLayerView = GeckoAppShell.getLayerView();
if (geckoAppLayerView != null) {
startZoomDisplay(geckoAppLayerView, left, top);
}
} else if (event.equals("Window:Resize")) {
ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
refreshZoomedViewSize(metrics);
} else if (event.equals("Content:LocationChange")) {
stopZoomDisplay();
}
} catch (JSONException e) {
Log.e(LOGTAG, "JSON exception", e);
}
}
});
}
private void moveUsingGeckoPosition(int leftFromGecko, int topFromGecko) {
if (layerView == null) {
return;
}
ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
convertedPosition = getZoomedViewTopLeftPositionFromTouchPosition((leftFromGecko * metrics.zoomFactor),
(topFromGecko * metrics.zoomFactor));
moveZoomedView(metrics, convertedPosition.x, convertedPosition.y);
}
@Override
public void onMetricsChanged(final ImmutableViewportMetrics viewport) {
// It can be called from a Gecko thread (forceViewportMetrics in GeckoLayerClient).
// Post to UI Thread to avoid Exception:
// "Only the original thread that created a view hierarchy can touch its views."
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
if (layerView == null) {
return;
}
shouldBlockUpdate(false);
refreshZoomedViewSize(viewport);
}
});
}
@Override
public void onPanZoomStopped() {
}
@Override
public void updateView(ByteBuffer data) {
final Bitmap sb3 = Bitmap.createBitmap(viewWidth, viewHeight, getBitmapConfig());
if (sb3 != null) {
data.rewind();
try {
sb3.copyPixelsFromBuffer(data);
} catch (Exception iae) {
Log.w(LOGTAG, iae.toString());
}
BitmapDrawable ob3 = new BitmapDrawable(getResources(), sb3);
if (zoomedImageView != null) {
zoomedImageView.setImageDrawable(ob3);
}
}
if (shouldSetVisibleOnUpdate) {
this.setVisibility(View.VISIBLE);
shouldSetVisibleOnUpdate = false;
}
lastStartTimeReRender = startTimeReRender;
startTimeReRender = 0;
}
private void updateBufferSize() {
int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2;
int capacity = viewWidth * viewHeight * pixelSize;
if (buffer == null || buffer.capacity() != capacity) {
buffer = DirectBufferAllocator.free(buffer);
buffer = DirectBufferAllocator.allocate(capacity);
}
}
private boolean isRendering() {
return (startTimeReRender != 0);
}
private boolean renderFrequencyTooHigh() {
return ((System.nanoTime() - lastStartTimeReRender) < MINIMUM_DELAY_BETWEEN_TWO_RENDER_CALLS_NS);
}
@Override
public void requestZoomedViewRender() {
if (stopUpdateView) {
return;
}
// remove pending runnable
ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable);
// "requestZoomedViewRender" can be called very often by Gecko (endDrawing in LayerRender) without
// any thing changed in the zoomed area (useless calls from the "zoomed area" point of view).
// "requestZoomedViewRender" can take time to re-render the zoomed view, it depends of the complexity
// of the html on this area.
// To avoid to slow down the application, the 2 following cases are tested:
// 1- Last render is still running, plan another render later.
if (isRendering()) {
// post a new runnable DELAY_BEFORE_NEXT_RENDER_REQUEST_MS later
// We need to post with a delay to be sure that the last call to requestZoomedViewRender will be done.
// For a static html page WITHOUT any animation/video, there is a last call to endDrawing and we need to make
// the zoomed render on this last call.
ThreadUtils.postDelayedToUiThread(requestRenderRunnable, DELAY_BEFORE_NEXT_RENDER_REQUEST_MS);
return;
}
// 2- Current render occurs too early, plan another render later.
if (renderFrequencyTooHigh()) {
// post a new runnable DELAY_BEFORE_NEXT_RENDER_REQUEST_MS later
// We need to post with a delay to be sure that the last call to requestZoomedViewRender will be done.
// For a page WITH animation/video, the animation/video can be stopped, and we need to make
// the zoomed render on this last call.
ThreadUtils.postDelayedToUiThread(requestRenderRunnable, DELAY_BEFORE_NEXT_RENDER_REQUEST_MS);
return;
}
startTimeReRender = System.nanoTime();
// Allocate the buffer if it's the first call.
// Change the buffer size if it's not the right size.
updateBufferSize();
int tabId = Tabs.getInstance().getSelectedTab().getId();
ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
PointF origin = metrics.getOrigin();
PointF offset = metrics.getMarginOffset();
final int xPos = (int) (origin.x - offset.x) + xLastPosition;
final int yPos = (int) (origin.y - offset.y) + yLastPosition;
GeckoEvent e = GeckoEvent.createZoomedViewEvent(tabId, xPos, yPos, viewWidth,
viewHeight, (float) (2.0 * metrics.zoomFactor), buffer);
GeckoAppShell.sendEventToGecko(e);
}
}

View File

@ -86,7 +86,8 @@ class GeckoLayerClient implements LayerView.Listener, PanZoomTarget
* that because mViewportMetrics might get reassigned in between reading the different
* fields. */
private volatile ImmutableViewportMetrics mViewportMetrics;
private LayerView.OnMetricsChangedListener mViewportChangeListener;
private LayerView.OnMetricsChangedListener mDynamicToolbarViewportChangeListener;
private LayerView.OnMetricsChangedListener mZoomedViewViewportChangeListener;
private ZoomConstraints mZoomConstraints;
@ -853,8 +854,11 @@ class GeckoLayerClient implements LayerView.Listener, PanZoomTarget
* You must hold the monitor while calling this.
*/
private void viewportMetricsChanged(boolean notifyGecko) {
if (mViewportChangeListener != null) {
mViewportChangeListener.onMetricsChanged(mViewportMetrics);
if (mDynamicToolbarViewportChangeListener != null) {
mDynamicToolbarViewportChangeListener.onMetricsChanged(mViewportMetrics);
}
if (mZoomedViewViewportChangeListener != null) {
mZoomedViewViewportChangeListener.onMetricsChanged(mViewportMetrics);
}
mView.requestRender();
@ -910,8 +914,11 @@ class GeckoLayerClient implements LayerView.Listener, PanZoomTarget
/** Implementation of PanZoomTarget */
@Override
public void panZoomStopped() {
if (mViewportChangeListener != null) {
mViewportChangeListener.onPanZoomStopped();
if (mDynamicToolbarViewportChangeListener != null) {
mDynamicToolbarViewportChangeListener.onPanZoomStopped();
}
if (mZoomedViewViewportChangeListener != null) {
mZoomedViewViewportChangeListener.onPanZoomStopped();
}
}
@ -982,8 +989,12 @@ class GeckoLayerClient implements LayerView.Listener, PanZoomTarget
return layerPoint;
}
void setOnMetricsChangedListener(LayerView.OnMetricsChangedListener listener) {
mViewportChangeListener = listener;
void setOnMetricsChangedDynamicToolbarViewportListener(LayerView.OnMetricsChangedListener listener) {
mDynamicToolbarViewportChangeListener = listener;
}
void setOnMetricsChangedZoomedViewportListener(LayerView.OnMetricsChangedListener listener) {
mZoomedViewViewportChangeListener = listener;
}
public void addDrawListener(DrawListener listener) {

View File

@ -26,13 +26,17 @@ import android.graphics.RectF;
import android.opengl.GLES20;
import android.os.SystemClock;
import android.util.Log;
import org.mozilla.gecko.mozglue.JNITarget;
import org.mozilla.gecko.util.ThreadUtils;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.ArrayList;
import java.util.List;
import javax.microedition.khronos.egl.EGLConfig;
@ -55,6 +59,8 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener {
private static final long NANOS_PER_MS = 1000000;
private static final int NANOS_PER_SECOND = 1000000000;
private static final int MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER = 5;
private final LayerView mView;
private final ScrollbarLayer mHorizScrollLayer;
private final ScrollbarLayer mVertScrollLayer;
@ -90,6 +96,10 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener {
private int mSampleHandle;
private int mTMatrixHandle;
private List<LayerView.OnZoomedViewListener> mZoomedViewListeners;
private float mViewLeft = 0.0f;
private float mViewTop = 0.0f;
// column-major matrix applied to each vertex to shift the viewport from
// one ranging from (-1, -1),(1,1) to (0,0),(1,1) and to scale all sizes by
// a factor of 2 to fill up the screen
@ -158,6 +168,7 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener {
mCoordBuffer = mCoordByteBuffer.asFloatBuffer();
Tabs.registerOnTabsChangedListener(this);
mZoomedViewListeners = new ArrayList<LayerView.OnZoomedViewListener>();
}
private Bitmap expandCanvasToPowerOfTwo(Bitmap image, IntSize size) {
@ -185,6 +196,7 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener {
mHorizScrollLayer.destroy();
mVertScrollLayer.destroy();
Tabs.unregisterOnTabsChangedListener(this);
mZoomedViewListeners.clear();
}
void onSurfaceCreated(EGLConfig config) {
@ -586,6 +598,41 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener {
}
public void maybeRequestZoomedViewRender(RenderContext context){
// Concurrently update of mZoomedViewListeners should not be an issue here
if (mZoomedViewListeners.size() == 0) {
return;
}
// When scrolling fast, do not request zoomed view render to avoid to slow down
// the scroll in the main view.
// Speed is estimated using the offset changes between 2 display frame calls
final float viewLeft = context.viewport.left - context.offset.x;
final float viewTop = context.viewport.top - context.offset.y;
boolean shouldWaitToRender = false;
if (Math.abs(mViewLeft - viewLeft) > MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER ||
Math.abs(mViewTop - viewTop) > MAX_SCROLL_SPEED_TO_REQUEST_ZOOM_RENDER) {
shouldWaitToRender = true;
}
mViewLeft = viewLeft;
mViewTop = viewTop;
if (shouldWaitToRender) {
return;
}
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
for (LayerView.OnZoomedViewListener listener : mZoomedViewListeners) {
listener.requestZoomedViewRender();
}
}
});
}
/** This function is invoked via JNI; be careful when modifying signature. */
@JNITarget
public void endDrawing() {
@ -595,6 +642,8 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener {
PanningPerfAPI.recordFrameTime();
maybeRequestZoomedViewRender(mPageContext);
/* Used by robocop for testing purposes */
IntBuffer pixelBuffer = mPixelBuffer;
if (mUpdated && pixelBuffer != null) {
@ -642,4 +691,25 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener {
}
}
}
public void updateZoomedView(final ByteBuffer data) {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
for (LayerView.OnZoomedViewListener listener : mZoomedViewListeners) {
listener.updateView(data);
}
}
});
}
public void addOnZoomedViewListener(LayerView.OnZoomedViewListener listener) {
ThreadUtils.assertOnUiThread();
mZoomedViewListeners.add(listener);
}
public void removeOnZoomedViewListener(LayerView.OnZoomedViewListener listener) {
ThreadUtils.assertOnUiThread();
mZoomedViewListeners.remove(listener);
}
}

View File

@ -5,7 +5,9 @@
package org.mozilla.gecko.gfx;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import org.mozilla.gecko.AndroidGamepadManager;
import org.mozilla.gecko.AppConstants.Versions;
@ -530,6 +532,19 @@ public class LayerView extends FrameLayout implements Tabs.OnTabsChangedListener
}
}
@WrapElementForJNI(allowMultithread = true, stubName = "updateZoomedView")
public static void updateZoomedView(ByteBuffer data) {
data.position(0);
LayerView layerView = GeckoAppShell.getLayerView();
if (layerView != null) {
LayerRenderer layerRenderer = layerView.getRenderer();
if (layerRenderer != null){
layerRenderer.updateZoomedView(data);
}
}
return;
}
public interface Listener {
void renderRequested();
void sizeChanged(int width, int height);
@ -662,7 +677,27 @@ public class LayerView extends FrameLayout implements Tabs.OnTabsChangedListener
public void onPanZoomStopped();
}
public void setOnMetricsChangedListener(OnMetricsChangedListener listener) {
mLayerClient.setOnMetricsChangedListener(listener);
public void setOnMetricsChangedDynamicToolbarViewportListener(OnMetricsChangedListener listener) {
mLayerClient.setOnMetricsChangedDynamicToolbarViewportListener(listener);
}
public void setOnMetricsChangedZoomedViewportListener(OnMetricsChangedListener listener) {
mLayerClient.setOnMetricsChangedZoomedViewportListener(listener);
}
// Public hooks for zoomed view
public interface OnZoomedViewListener {
public void requestZoomedViewRender();
public void updateView(ByteBuffer data);
}
public void addOnZoomedViewListener(OnZoomedViewListener listener) {
mRenderer.addOnZoomedViewListener(listener);
}
public void removeOnZoomedViewListener(OnZoomedViewListener listener) {
mRenderer.removeOnZoomedViewListener(listener);
}
}

View File

@ -500,6 +500,7 @@ gbjar.sources += [
'widget/ThumbnailView.java',
'widget/TwoWayView.java',
'ZoomConstraints.java',
'ZoomedView.java',
]
# The following sources are checked in to version control but
# generated by a script (widget/generate_themed_views.py). If you're

View File

@ -25,6 +25,7 @@
android:visibility="gone"/>
<include layout="@layout/text_selection_handles"/>
<include layout="@layout/zoomed_view"/>
<FrameLayout android:id="@+id/camera_layout"
android:layout_height="wrap_content"

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<org.mozilla.gecko.ZoomedView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res-auto"
android:id="@+id/zoomed_view_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:background="@android:color/white"
android:visibility="gone" >
<ImageView
android:id="@+id/zoomed_image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#000000"
android:padding="1dip" />
<ImageView
android:id="@+id/dialog_close"
android:background="@drawable/close"
android:layout_height="20dp"
android:layout_width="20dp"
android:layout_gravity ="top|right" />
</org.mozilla.gecko.ZoomedView>

View File

@ -96,6 +96,14 @@ public final class ThreadUtils {
sUiHandler.post(runnable);
}
public static void postDelayedToUiThread(Runnable runnable, long timeout) {
sUiHandler.postDelayed(runnable, timeout);
}
public static void removeCallbacksFromUiThread(Runnable runnable) {
sUiHandler.removeCallbacks(runnable);
}
public static Thread getBackgroundThread() {
return sBackgroundThread;
}

View File

@ -4980,7 +4980,12 @@ var BrowserEventHandler = {
if (!target) {
return;
}
this._inCluster = aEvent.hitCluster;
if (this._inCluster) {
return; // No highlight for a cluster of links
}
let uri = this._getLinkURI(target);
if (uri) {
try {
@ -5097,6 +5102,7 @@ var BrowserEventHandler = {
let data = JSON.parse(aData);
let {x, y} = data;
if (this._inCluster) {
this._clusterClicked(x, y);
} else {
@ -5129,14 +5135,14 @@ var BrowserEventHandler = {
}
},
_clusterClicked: function sh_clusterClicked(aX, aY) {
Messaging.sendRequest({
type: "Gesture:clusteredLinksClicked",
clicPosition: {
x: aX,
y: aY
}
});
_clusterClicked: function(aX, aY) {
Messaging.sendRequest({
type: "Gesture:clusteredLinksClicked",
clickPosition: {
x: aX,
y: aY
}
});
},
onDoubleTap: function(aData) {

View File

@ -1704,6 +1704,94 @@ AndroidBridge::GetFrameNameJavaProfiling(uint32_t aThreadId, uint32_t aSampleId,
return true;
}
static float
GetScaleFactor(nsPresContext* mPresContext) {
nsIPresShell* presShell = mPresContext->PresShell();
LayoutDeviceToLayerScale cumulativeResolution(presShell->GetCumulativeResolution().width);
return cumulativeResolution.scale;
}
nsresult
AndroidBridge::CaptureZoomedView (nsIDOMWindow *window, nsIntRect zoomedViewRect, Object::Param buffer,
float zoomFactor) {
nsresult rv;
struct timeval timeEnd;
struct timeval timeEndAfter;
struct timeval timeStart;
struct timeval res;
gettimeofday (&timeStart, NULL);
if (!buffer)
return NS_ERROR_FAILURE;
nsCOMPtr < nsIDOMWindowUtils > utils = do_GetInterface (window);
if (!utils)
return NS_ERROR_FAILURE;
JNIEnv* env = GetJNIEnv ();
AutoLocalJNIFrame jniFrame (env, 0);
nsCOMPtr < nsPIDOMWindow > win = do_QueryInterface (window);
if (!win) {
return NS_ERROR_FAILURE;
}
nsRefPtr < nsPresContext > presContext;
nsIDocShell* docshell = win->GetDocShell ();
if (docshell) {
docshell->GetPresContext (getter_AddRefs (presContext));
}
if (!presContext) {
return NS_ERROR_FAILURE;
}
nsCOMPtr < nsIPresShell > presShell = presContext->PresShell ();
float scaleFactor = GetScaleFactor(presContext) ;
nscolor bgColor = NS_RGB (255, 255, 255);
uint32_t renderDocFlags = (nsIPresShell::RENDER_IGNORE_VIEWPORT_SCROLLING | nsIPresShell::RENDER_DOCUMENT_RELATIVE);
nsRect r (presContext->DevPixelsToAppUnits(zoomedViewRect.x / scaleFactor),
presContext->DevPixelsToAppUnits(zoomedViewRect.y / scaleFactor ),
presContext->DevPixelsToAppUnits(zoomedViewRect.width / scaleFactor ),
presContext->DevPixelsToAppUnits(zoomedViewRect.height / scaleFactor ));
bool is24bit = (GetScreenDepth () == 24);
SurfaceFormat format = is24bit ? SurfaceFormat::B8G8R8X8 : SurfaceFormat::R5G6B5;
gfxImageFormat iFormat = gfx::SurfaceFormatToImageFormat(format);
uint32_t stride = gfxASurface::FormatStrideForWidth(iFormat, zoomedViewRect.width);
uint8_t* data = static_cast<uint8_t*> (env->GetDirectBufferAddress (buffer.Get()));
if (!data) {
return NS_ERROR_FAILURE;
}
MOZ_ASSERT (gfxPlatform::GetPlatform ()->SupportsAzureContentForType (BackendType::CAIRO),
"Need BackendType::CAIRO support");
RefPtr < DrawTarget > dt = Factory::CreateDrawTargetForData (
BackendType::CAIRO, data, IntSize (zoomedViewRect.width, zoomedViewRect.height), stride,
format);
if (!dt) {
ALOG_BRIDGE ("Error creating DrawTarget");
return NS_ERROR_FAILURE;
}
nsRefPtr < gfxContext > context = new gfxContext (dt);
context->SetMatrix (context->CurrentMatrix ().Scale(zoomFactor, zoomFactor));
rv = presShell->RenderDocument (r, renderDocFlags, bgColor, context);
if (is24bit) {
gfxUtils::ConvertBGRAtoRGBA (data, stride * zoomedViewRect.height);
}
LayerView::updateZoomedView(buffer);
NS_ENSURE_SUCCESS (rv, rv);
return NS_OK;
}
nsresult AndroidBridge::CaptureThumbnail(nsIDOMWindow *window, int32_t bufW, int32_t bufH, int32_t tabId, Object::Param buffer, bool &shouldStore)
{
nsresult rv;

View File

@ -188,6 +188,7 @@ public:
bool GetThreadNameJavaProfiling(uint32_t aThreadId, nsCString & aResult);
bool GetFrameNameJavaProfiling(uint32_t aThreadId, uint32_t aSampleId, uint32_t aFrameId, nsCString & aResult);
nsresult CaptureZoomedView(nsIDOMWindow *window, nsIntRect zoomedViewRect, jni::Object::Param buffer, float zoomFactor);
nsresult CaptureThumbnail(nsIDOMWindow *window, int32_t bufW, int32_t bufH, int32_t tabId, jni::Object::Param buffer, bool &shouldStore);
void GetDisplayPort(bool aPageSizeUpdate, bool aIsBrowserContentDisplayed, int32_t tabId, nsIAndroidViewport* metrics, nsIAndroidDisplayport** displayPort);
void ContentDocumentChanged();

View File

@ -538,6 +538,14 @@ AndroidGeckoEvent::Init(JNIEnv *jenv, jobject jobj)
break;
}
case ZOOMEDVIEW: {
mX = jenv->GetDoubleField(jobj, jXField);
mMetaState = jenv->GetIntField(jobj, jMetaStateField);
ReadPointArray(mPoints, jenv, jPoints, 2);
mByteBuffer = new RefCountedJavaObject(jenv, jenv->GetObjectField(jobj, jByteBufferField));
break;
}
case SCREENORIENTATION_CHANGED: {
mScreenOrientation = jenv->GetShortField(jobj, jScreenOrientationField);
break;

View File

@ -746,6 +746,7 @@ public:
GAMEPAD_ADDREMOVE = 45,
GAMEPAD_DATA = 46,
LONG_PRESS = 47,
ZOOMEDVIEW = 48,
dummy_java_enum_list_end
};

View File

@ -980,6 +980,14 @@ mozilla::jni::Object::LocalRef LayerView::RegisterCompositorWrapper()
return mozilla::jni::Method<RegisterCompositorWrapper_t>::Call(nullptr, nullptr);
}
constexpr char LayerView::updateZoomedView_t::name[];
constexpr char LayerView::updateZoomedView_t::signature[];
void LayerView::updateZoomedView(mozilla::jni::Object::Param a0)
{
return mozilla::jni::Method<updateZoomedView_t>::Call(nullptr, nullptr, a0);
}
constexpr char NativePanZoomController::name[];
constexpr char NativePanZoomController::RequestContentRepaintWrapper_t::name[];

View File

@ -1934,6 +1934,21 @@ public:
static mozilla::jni::Object::LocalRef RegisterCompositorWrapper();
public:
struct updateZoomedView_t {
typedef LayerView Owner;
typedef void ReturnType;
typedef void SetterType;
static constexpr char name[] = "updateZoomedView";
static constexpr char signature[] =
"(Ljava/nio/ByteBuffer;)V";
static const bool isStatic = true;
static const bool isMultithreaded = true;
static const mozilla::jni::ExceptionMode exceptionMode = mozilla::jni::ExceptionMode::ABORT;
};
static void updateZoomedView(mozilla::jni::Object::Param);
};
class NativePanZoomController : public mozilla::jni::Class<NativePanZoomController> {

View File

@ -393,6 +393,33 @@ nsAppShell::ProcessNextNativeEvent(bool mayWait)
break;
}
case AndroidGeckoEvent::ZOOMEDVIEW: {
if (!mBrowserApp)
break;
int32_t tabId = curEvent->MetaState();
const nsTArray<nsIntPoint>& points = curEvent->Points();
float scaleFactor = (float) curEvent->X();
nsRefPtr<RefCountedJavaObject> javaBuffer = curEvent->ByteBuffer();
const auto& mBuffer = jni::Object::Ref::From(javaBuffer->GetObject());
nsCOMPtr<nsIDOMWindow> domWindow;
nsCOMPtr<nsIBrowserTab> tab;
mBrowserApp->GetBrowserTab(tabId, getter_AddRefs(tab));
if (!tab) {
NS_ERROR("Can't find tab!");
break;
}
tab->GetWindow(getter_AddRefs(domWindow));
if (!domWindow) {
NS_ERROR("Can't find dom window!");
break;
}
NS_ASSERTION(points.Length() == 2, "ZoomedView event does not have enough coordinates");
nsIntRect r(points[0].x, points[0].y, points[1].x, points[1].y);
nsresult rv = AndroidBridge::Bridge()->CaptureZoomedView(domWindow, r, mBuffer, scaleFactor);
break;
}
case AndroidGeckoEvent::VIEWPORT:
case AndroidGeckoEvent::BROADCAST: {
if (curEvent->Characters().Length() == 0)