/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3; import android.content.Context; import android.graphics.Point; import android.graphics.RectF; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.ScaleGestureDetector.OnScaleGestureListener; import android.view.ViewConfiguration; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import com.android.photos.views.TiledImageRenderer.TileSource; import com.android.photos.views.TiledImageView; public class CropView extends TiledImageView implements OnScaleGestureListener { private ScaleGestureDetector mScaleGestureDetector; private long mTouchDownTime; private float mFirstX, mFirstY; private float mLastX, mLastY; private float mMinScale; private boolean mTouchEnabled = true; private RectF mTempEdges = new RectF(); TouchCallback mTouchCallback; public interface TouchCallback { void onTouchDown(); void onTap(); void onTouchUp(); } public CropView(Context context) { this(context, null); } public CropView(Context context, AttributeSet attrs) { super(context, attrs); mScaleGestureDetector = new ScaleGestureDetector(context, this); } private void getEdgesHelper(RectF edgesOut) { final float width = getWidth(); final float height = getHeight(); final float imageWidth = mRenderer.source.getImageWidth(); final float imageHeight = mRenderer.source.getImageHeight(); final float scale = mRenderer.scale; float centerX = (width / 2f - mRenderer.centerX + (imageWidth - width) / 2f) * scale + width / 2f; float centerY = (height / 2f - mRenderer.centerY + (imageHeight - height) / 2f) * scale + height / 2f; float leftEdge = centerX - imageWidth / 2f * scale; float rightEdge = centerX + imageWidth / 2f * scale; float topEdge = centerY - imageHeight / 2f * scale; float bottomEdge = centerY + imageHeight / 2f * scale; edgesOut.left = leftEdge; edgesOut.right = rightEdge; edgesOut.top = topEdge; edgesOut.bottom = bottomEdge; } public RectF getCrop() { final RectF edges = mTempEdges; getEdgesHelper(edges); final float scale = mRenderer.scale; float cropLeft = -edges.left / scale; float cropTop = -edges.top / scale; float cropRight = cropLeft + getWidth() / scale; float cropBottom = cropTop + getHeight() / scale; return new RectF(cropLeft, cropTop, cropRight, cropBottom); } public Point getSourceDimensions() { return new Point(mRenderer.source.getImageWidth(), mRenderer.source.getImageHeight()); } public void setTileSource(TileSource source, Runnable isReadyCallback) { super.setTileSource(source, isReadyCallback); updateMinScale(getWidth(), getHeight(), source, true); } protected void onSizeChanged(int w, int h, int oldw, int oldh) { updateMinScale(w, h, mRenderer.source, false); } public void setScale(float scale) { synchronized (mLock) { mRenderer.scale = scale; } } private void updateMinScale(int w, int h, TileSource source, boolean resetScale) { synchronized (mLock) { if (resetScale) { mRenderer.scale = 1; } if (source != null) { mMinScale = Math.max(w / (float) source.getImageWidth(), h / (float) source.getImageHeight()); mRenderer.scale = Math.max(mMinScale, mRenderer.scale); } } } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { return true; } @Override public boolean onScale(ScaleGestureDetector detector) { // Don't need the lock because this will only fire inside of // onTouchEvent mRenderer.scale *= detector.getScaleFactor(); mRenderer.scale = Math.max(mMinScale, mRenderer.scale); invalidate(); return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { } public void moveToLeft() { if (getWidth() == 0 || getHeight() == 0) { final ViewTreeObserver observer = getViewTreeObserver(); observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { public void onGlobalLayout() { moveToLeft(); getViewTreeObserver().removeOnGlobalLayoutListener(this); } }); } final RectF edges = mTempEdges; getEdgesHelper(edges); final float scale = mRenderer.scale; mRenderer.centerX += Math.ceil(edges.left / scale); } public void setTouchEnabled(boolean enabled) { mTouchEnabled = enabled; } public void setTouchCallback(TouchCallback cb) { mTouchCallback = cb; } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getActionMasked(); final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP; final int skipIndex = pointerUp ? event.getActionIndex() : -1; // Determine focal point float sumX = 0, sumY = 0; final int count = event.getPointerCount(); for (int i = 0; i < count; i++) { if (skipIndex == i) continue; sumX += event.getX(i); sumY += event.getY(i); } final int div = pointerUp ? count - 1 : count; float x = sumX / div; float y = sumY / div; if (action == MotionEvent.ACTION_DOWN) { mFirstX = x; mFirstY = y; mTouchDownTime = System.currentTimeMillis(); if (mTouchCallback != null) { mTouchCallback.onTouchDown(); } } else if (action == MotionEvent.ACTION_UP) { ViewConfiguration config = ViewConfiguration.get(getContext()); float squaredDist = (mFirstX - x) * (mFirstX - x) + (mFirstY - y) * (mFirstY - y); float slop = config.getScaledTouchSlop() * config.getScaledTouchSlop(); long now = System.currentTimeMillis(); // only do this if it's a small movement if (mTouchCallback != null && squaredDist < slop && now < mTouchDownTime + ViewConfiguration.getTapTimeout()) { mTouchCallback.onTap(); } mTouchCallback.onTouchUp(); } if (!mTouchEnabled) { return true; } synchronized (mLock) { mScaleGestureDetector.onTouchEvent(event); switch (action) { case MotionEvent.ACTION_MOVE: mRenderer.centerX += (mLastX - x) / mRenderer.scale; mRenderer.centerY += (mLastY - y) / mRenderer.scale; invalidate(); break; } if (mRenderer.source != null) { // Adjust position so that the wallpaper covers the entire area // of the screen final RectF edges = mTempEdges; getEdgesHelper(edges); final float scale = mRenderer.scale; if (edges.left > 0) { mRenderer.centerX += Math.ceil(edges.left / scale); } if (edges.right < getWidth()) { mRenderer.centerX += (edges.right - getWidth()) / scale; } if (edges.top > 0) { mRenderer.centerY += Math.ceil(edges.top / scale); } if (edges.bottom < getHeight()) { mRenderer.centerY += (edges.bottom - getHeight()) / scale; } } } mLastX = x; mLastY = y; return true; } }