Android Open Source - Muzei Pan View






From Project

Back to project page Muzei.

License

The source code is released under:

Apache License

If you think the Android project Muzei listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
 * Copyright 2014 Google Inc.//from   w ww .  j av  a 2  s . com
 *
 * 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.google.android.apps.muzei.util;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.v4.os.ParcelableCompat;
import android.support.v4.os.ParcelableCompatCreatorCallbacks;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.EdgeEffect;
import android.widget.OverScroller;

/**
 * View which supports panning around an image larger than the screen size. Supports both scrolling
 * and flinging
 */
public class PanView extends View {
    private static final String TAG = PanView.class.getSimpleName();

    private Bitmap mImage;
    private Bitmap mScaledImage;

    private Bitmap mBlurredImage;
    private float mBlurAmount = 0f;
    private Paint mDrawBlurredPaint;

    /**
     * Horizontal offset for painting the image. As this is used in a canvas.drawBitmap it ranges
     * from a negative value mWidth-image.getWidth() (remember the view is smaller than the image)
     * to zero. If it is zero that means the offsetX side of the image is visible otherwise it is
     * off screen and we are farther to the right.
     */
    private float mOffsetX;
    /**
     * Vertical offset for painting the image. As this is used in a canvas.drawBitmap it ranges
     * from a negative value mHeight-image.getHeight() (remember the view is smaller than the image)
     * to zero. If it is zero that means the offsetY side of the image is visible otherwise it is
     * off screen and we are farther down.
     */
    private float mOffsetY;
    /**
     * View width
     */
    private int mWidth = 1;
    /**
     * View height
     */
    private int mHeight = 1;

    // State objects and values related to gesture tracking.
    private GestureDetector mGestureDetector;
    private OverScroller mScroller;
    /**
     * Handler for posting fling animation updates
     */
    private Handler mHandler = new Handler();

    // Edge effect / overscroll tracking objects.
    private EdgeEffect mEdgeEffectTop;
    private EdgeEffect mEdgeEffectBottom;
    private EdgeEffect mEdgeEffectLeft;
    private EdgeEffect mEdgeEffectRight;

    private boolean mEdgeEffectTopActive;
    private boolean mEdgeEffectBottomActive;
    private boolean mEdgeEffectLeftActive;
    private boolean mEdgeEffectRightActive;

    public PanView(Context context) {
        this(context, null, 0);
    }

    public PanView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PanView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        // Sets up interactions
        mGestureDetector = new GestureDetector(context, new ScrollFlingGestureListener());
        mScroller = new OverScroller(context);
        mEdgeEffectLeft = new EdgeEffect(context);
        mEdgeEffectTop = new EdgeEffect(context);
        mEdgeEffectRight = new EdgeEffect(context);
        mEdgeEffectBottom = new EdgeEffect(context);

        mDrawBlurredPaint = new Paint();
        mDrawBlurredPaint.setDither(true);
    }

    /**
     * Sets an image to be displayed. Preferably this image should be larger than this view's size
     * to allow scrolling. Note that the image will be centered on first display
     * @param image Image to display
     */
    public void setImage(Bitmap image) {
        mImage = image;
        updateScaledImage();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = Math.max(1, w);
        mHeight = Math.max(1, h);
        updateScaledImage();
    }

    private void updateScaledImage() {
        if (mImage == null) {
            return;
        }
        int width = mImage.getWidth();
        int height = mImage.getHeight();
        if (width > height) {
            float scalingFactor = mHeight * 1f / height;
            mScaledImage = Bitmap.createScaledBitmap(mImage, (int)(scalingFactor * width), mHeight, true);
        } else {
            float scalingFactor = mWidth * 1f / width;
            mScaledImage = Bitmap.createScaledBitmap(mImage, mWidth, (int)(scalingFactor * height), true);
        }
        ImageBlurrer blurrer = new ImageBlurrer(getContext());
        mBlurredImage = blurrer.blurBitmap(mScaledImage,
                ImageBlurrer.MAX_SUPPORTED_BLUR_PIXELS, 0f);
        blurrer.destroy();
        // Center the image
        mOffsetX = (mWidth - mScaledImage.getWidth()) / 2;
        mOffsetY = (mHeight - mScaledImage.getHeight()) / 2;
        invalidate();
    }

    public void setBlurAmount(float blurAmount) {
        mBlurAmount = blurAmount;
        postInvalidateOnAnimation();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mBlurAmount < 1f) {
            if (mScaledImage != null) {
                canvas.drawBitmap(mScaledImage, mOffsetX, mOffsetY, null);
            }
        }
        if (mBlurAmount > 0f) {
            if (mBlurredImage != null) {
                mDrawBlurredPaint.setAlpha((int) (mBlurAmount * 255));
                canvas.drawBitmap(mBlurredImage, mOffsetX, mOffsetY, mDrawBlurredPaint);
            }
        }
        drawEdgeEffects(canvas);
    }

    /**
     * Draws the overscroll "glow" at the four edges, if necessary
     *
     * @see EdgeEffect
     */
    private void drawEdgeEffects(Canvas canvas) {
        // The methods below rotate and translate the canvas as needed before drawing the glow,
        // since EdgeEffect always draws a top-glow at 0,0.

        boolean needsInvalidate = false;

        if (!mEdgeEffectTop.isFinished()) {
            final int restoreCount = canvas.save();
            mEdgeEffectTop.setSize(mWidth, mHeight);
            if (mEdgeEffectTop.draw(canvas)) {
                needsInvalidate = true;
            }
            canvas.restoreToCount(restoreCount);
        }

        if (!mEdgeEffectBottom.isFinished()) {
            final int restoreCount = canvas.save();
            canvas.translate(-mWidth, mHeight);
            canvas.rotate(180, mWidth, 0);
            mEdgeEffectBottom.setSize(mWidth, mHeight);
            if (mEdgeEffectBottom.draw(canvas)) {
                needsInvalidate = true;
            }
            canvas.restoreToCount(restoreCount);
        }

        if (!mEdgeEffectLeft.isFinished()) {
            final int restoreCount = canvas.save();
            canvas.translate(0, mHeight);
            canvas.rotate(-90, 0, 0);
            //noinspection SuspiciousNameCombination
            mEdgeEffectLeft.setSize(mHeight, mWidth);
            if (mEdgeEffectLeft.draw(canvas)) {
                needsInvalidate = true;
            }
            canvas.restoreToCount(restoreCount);
        }

        if (!mEdgeEffectRight.isFinished()) {
            final int restoreCount = canvas.save();
            canvas.translate(mWidth, 0);
            canvas.rotate(90, 0, 0);
            //noinspection SuspiciousNameCombination
            mEdgeEffectRight.setSize(mHeight, mWidth);
            if (mEdgeEffectRight.draw(canvas)) {
                needsInvalidate = true;
            }
            canvas.restoreToCount(restoreCount);
        }

        if (needsInvalidate) {
            invalidate();
        }
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////
    //
    //     Methods and objects related to gesture handling
    //
    ////////////////////////////////////////////////////////////////////////////////////////////////

    @Override
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        return mGestureDetector.onTouchEvent(event) || super.onTouchEvent(event);
    }

    private void setOffset(float offsetX, float offsetY) {
        if (mScaledImage == null) {
            return;
        }

        // Constrain between mWidth - mScaledImage.getWidth() and 0
        // mWidth - mScaledImage.getWidth() -> right edge visible
        // 0 -> left edge visible
        mOffsetX = Math.min(0, Math.max(mWidth - mScaledImage.getWidth(), offsetX));
        // Constrain between mHeight - mScaledImage.getHeight() and 0
        // mHeight - mScaledImage.getHeight() -> bottom edge visible
        // 0 -> top edge visible
        mOffsetY = Math.min(0, Math.max(mHeight - mScaledImage.getHeight(), offsetY));
    }

    /**
     * The gesture listener, used for handling simple gestures such as scrolls and flings.
     */
    private class ScrollFlingGestureListener extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onDown(MotionEvent e) {
            releaseEdgeEffects();
            mScroller.forceFinished(true);
            invalidate();
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            if (mScaledImage == null) {
                return true;
            }

            float offsetX = mOffsetX;
            float offsetY = mOffsetY;
            setOffset(mOffsetX - distanceX, mOffsetY - distanceY);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                Log.v(TAG, "Scrolling to " + mOffsetX + ", " + mOffsetY);
            }
            if (mWidth != mScaledImage.getWidth() && mOffsetX < offsetX - distanceX) {
                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.v(TAG, "Left edge pulled " + -distanceX);
                }
                mEdgeEffectLeft.onPull(-distanceX * 1f / mWidth);
                mEdgeEffectLeftActive = true;
            }
            if (mHeight != mScaledImage.getHeight() && mOffsetY < offsetY - distanceY) {
                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.v(TAG, "Top edge pulled " + distanceY);
                }
                mEdgeEffectTop.onPull(-distanceY * 1f / mHeight);
                mEdgeEffectTopActive = true;
            }
            if (mHeight != mScaledImage.getHeight() && mOffsetY > offsetY - distanceY) {
                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.v(TAG, "Bottom edge pulled " + -distanceY);
                }
                mEdgeEffectBottom.onPull(distanceY * 1f / mHeight);
                mEdgeEffectBottomActive = true;
            }
            if (mWidth != mScaledImage.getWidth() && mOffsetX > offsetX - distanceX) {
                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.v(TAG, "Right edge pulled " + distanceX);
                }
                mEdgeEffectRight.onPull(distanceX * 1f / mWidth);
                mEdgeEffectRightActive = true;
            }
            invalidate();
            return true;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            if (mScaledImage == null) {
                return true;
            }

            releaseEdgeEffects();
            mScroller.forceFinished(true);
            mScroller.fling(
                    (int) mOffsetX,
                    (int) mOffsetY,
                    (int) velocityX,
                    (int) velocityY,
                    mWidth - mScaledImage.getWidth(), 0, // mWidth - mScaledImage.getWidth() is negative
                    mHeight - mScaledImage.getHeight(), 0, // mHeight - mScaledImage.getHeight() is negative
                    mScaledImage.getWidth() / 2,
                    mScaledImage.getHeight() / 2);
            postAnimateTick();
            invalidate();
            return true;
        }

        private void releaseEdgeEffects() {
            mEdgeEffectLeftActive
                    = mEdgeEffectTopActive
                    = mEdgeEffectRightActive
                    = mEdgeEffectBottomActive
                    = false;
            mEdgeEffectLeft.onRelease();
            mEdgeEffectTop.onRelease();
            mEdgeEffectRight.onRelease();
            mEdgeEffectBottom.onRelease();
        }
    }

    private void postAnimateTick() {
        mHandler.removeCallbacks(mAnimateTickRunnable);
        mHandler.post(mAnimateTickRunnable);
    }

    private Runnable mAnimateTickRunnable = new Runnable() {
        @Override
        public void run() {
            boolean needsInvalidate = false;

            if (mScroller.computeScrollOffset()) {
                // The scroller isn't finished, meaning a fling is currently active.
                setOffset(mScroller.getCurrX(), mScroller.getCurrY());

                if (mWidth != mScaledImage.getWidth() && mOffsetX < mScroller.getCurrX()
                        && mEdgeEffectLeft.isFinished()
                        && !mEdgeEffectLeftActive) {
                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
                        Log.v(TAG, "Left edge absorbing " + mScroller.getCurrVelocity());
                    }
                    mEdgeEffectLeft.onAbsorb((int) mScroller.getCurrVelocity());
                    mEdgeEffectLeftActive = true;
                } else if (mWidth != mScaledImage.getWidth() && mOffsetX > mScroller.getCurrX()
                        && mEdgeEffectRight.isFinished()
                        && !mEdgeEffectRightActive) {
                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
                        Log.v(TAG, "Right edge absorbing " + mScroller.getCurrVelocity());
                    }
                    mEdgeEffectRight.onAbsorb((int) mScroller.getCurrVelocity());
                    mEdgeEffectRightActive = true;
                }

                if (mHeight != mScaledImage.getHeight() && mOffsetY < mScroller.getCurrY()
                        && mEdgeEffectTop.isFinished()
                        && !mEdgeEffectTopActive) {
                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
                        Log.v(TAG, "Top edge absorbing " + mScroller.getCurrVelocity());
                    }
                    mEdgeEffectTop.onAbsorb((int) mScroller.getCurrVelocity());
                    mEdgeEffectTopActive = true;
                } else if (mHeight != mScaledImage.getHeight() && mOffsetY > mScroller.getCurrY()
                        && mEdgeEffectBottom.isFinished()
                        && !mEdgeEffectBottomActive) {
                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
                        Log.v(TAG, "Bottom edge absorbing " + mScroller.getCurrVelocity());
                    }
                    mEdgeEffectBottom.onAbsorb((int) mScroller.getCurrVelocity());
                    mEdgeEffectBottomActive = true;
                }

                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.v(TAG, "Flinging to " + mOffsetX + ", " + mOffsetY);
                }
                needsInvalidate = true;
            }

            if (needsInvalidate) {
                invalidate();
                postAnimateTick();
            }
        }
    };

    ////////////////////////////////////////////////////////////////////////////////////////////////
    //
    //     Methods and classes related to view state persistence.
    //
    ////////////////////////////////////////////////////////////////////////////////////////////////

    @Override
    public Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        SavedState ss = new SavedState(superState);
        ss.offsetX = mOffsetX;
        ss.offsetY = mOffsetY;
        return ss;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        if (!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }

        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());

        mOffsetX = ss.offsetX;
        mOffsetY = ss.offsetY;
    }

    /**
     * Persistent state that is saved by PanView.
     */
    public static class SavedState extends BaseSavedState {
        private float offsetX;
        private float offsetY;

        public SavedState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(@NonNull Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeFloat(offsetX);
            out.writeFloat(offsetY);
        }

        @Override
        public String toString() {
            return "PanView.SavedState{"
                    + Integer.toHexString(System.identityHashCode(this))
                    + " offset=" + offsetX + ", " + offsetY + "}";
        }

        public static final Creator<SavedState> CREATOR
                = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() {
            @Override
            public SavedState createFromParcel(Parcel in, ClassLoader loader) {
                return new SavedState(in);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        });

        SavedState(Parcel in) {
            super(in);
            offsetX = in.readFloat();
            offsetY = in.readFloat();
        }
    }
}




Java Source Code List

com.example.muzei.examplecontractwidget.ArtworkUpdateReceiver.java
com.example.muzei.examplecontractwidget.ArtworkUpdateService.java
com.example.muzei.examplecontractwidget.MuzeiAppWidgetProvider.java
com.example.muzei.examplesource500px.Config.java
com.example.muzei.examplesource500px.FiveHundredPxExampleArtSource.java
com.example.muzei.examplesource500px.FiveHundredPxService.java
com.example.muzei.watchface.ArtworkImageLoader.java
com.example.muzei.watchface.MuzeiExampleWatchface.java
com.example.muzei.watchface.WatchfaceArtworkImageLoader.java
com.google.android.apps.muzei.ActivateMuzeiIntentService.java
com.google.android.apps.muzei.ArtDetailViewport.java
com.google.android.apps.muzei.ArtworkCacheIntentService.java
com.google.android.apps.muzei.ArtworkCache.java
com.google.android.apps.muzei.FullScreenActivity.java
com.google.android.apps.muzei.LockScreenVisibleReceiver.java
com.google.android.apps.muzei.MuzeiActivity.java
com.google.android.apps.muzei.MuzeiApplication.java
com.google.android.apps.muzei.MuzeiWallpaperService.java
com.google.android.apps.muzei.MuzeiWatchFace.java
com.google.android.apps.muzei.MuzeiWearableListenerService.java
com.google.android.apps.muzei.MuzeiWearableListenerService.java
com.google.android.apps.muzei.NetworkChangeReceiver.java
com.google.android.apps.muzei.NewWallpaperNotificationReceiver.java
com.google.android.apps.muzei.PhotoSetAsTargetActivity.java
com.google.android.apps.muzei.SourceManager.java
com.google.android.apps.muzei.SourcePackageChangeReceiver.java
com.google.android.apps.muzei.SourceSubscriberService.java
com.google.android.apps.muzei.TaskQueueService.java
com.google.android.apps.muzei.WearableController.java
com.google.android.apps.muzei.api.Artwork.java
com.google.android.apps.muzei.api.MuzeiArtSource.java
com.google.android.apps.muzei.api.MuzeiContract.java
com.google.android.apps.muzei.api.RemoteMuzeiArtSource.java
com.google.android.apps.muzei.api.UserCommand.java
com.google.android.apps.muzei.api.internal.ProtocolConstants.java
com.google.android.apps.muzei.api.internal.SourceState.java
com.google.android.apps.muzei.event.ArtDetailOpenedClosedEvent.java
com.google.android.apps.muzei.event.ArtworkLoadingStateChangedEvent.java
com.google.android.apps.muzei.event.ArtworkSizeChangedEvent.java
com.google.android.apps.muzei.event.BlurAmountChangedEvent.java
com.google.android.apps.muzei.event.CurrentArtworkDownloadedEvent.java
com.google.android.apps.muzei.event.DimAmountChangedEvent.java
com.google.android.apps.muzei.event.GainedNetworkConnectivityEvent.java
com.google.android.apps.muzei.event.GalleryChosenUrisChangedEvent.java
com.google.android.apps.muzei.event.GreyAmountChangedEvent.java
com.google.android.apps.muzei.event.LockScreenVisibleChangedEvent.java
com.google.android.apps.muzei.event.SelectedSourceChangedEvent.java
com.google.android.apps.muzei.event.SelectedSourceStateChangedEvent.java
com.google.android.apps.muzei.event.SwitchingPhotosStateChangedEvent.java
com.google.android.apps.muzei.event.WallpaperActiveStateChangedEvent.java
com.google.android.apps.muzei.event.WallpaperSizeChangedEvent.java
com.google.android.apps.muzei.featuredart.FeaturedArtSource.java
com.google.android.apps.muzei.gallery.GalleryArtSource.java
com.google.android.apps.muzei.gallery.GalleryDatabase.java
com.google.android.apps.muzei.gallery.GalleryEmptyStateGraphicView.java
com.google.android.apps.muzei.gallery.GallerySettingsActivity.java
com.google.android.apps.muzei.gallery.GalleryStore.java
com.google.android.apps.muzei.provider.MuzeiProvider.java
com.google.android.apps.muzei.render.BitmapRegionLoader.java
com.google.android.apps.muzei.render.DemoRenderController.java
com.google.android.apps.muzei.render.GLColorOverlay.java
com.google.android.apps.muzei.render.GLPicture.java
com.google.android.apps.muzei.render.GLTextureView.java
com.google.android.apps.muzei.render.GLUtil.java
com.google.android.apps.muzei.render.ImageUtil.java
com.google.android.apps.muzei.render.MuzeiBlurRenderer.java
com.google.android.apps.muzei.render.MuzeiRendererFragment.java
com.google.android.apps.muzei.render.RealRenderController.java
com.google.android.apps.muzei.render.RenderController.java
com.google.android.apps.muzei.settings.AboutActivity.java
com.google.android.apps.muzei.settings.Prefs.java
com.google.android.apps.muzei.settings.SettingsActivity.java
com.google.android.apps.muzei.settings.SettingsAdvancedFragment.java
com.google.android.apps.muzei.settings.SettingsChooseSourceFragment.java
com.google.android.apps.muzei.util.AnimatedMuzeiLoadingSpinnerView.java
com.google.android.apps.muzei.util.AnimatedMuzeiLogoFragment.java
com.google.android.apps.muzei.util.AnimatedMuzeiLogoView.java
com.google.android.apps.muzei.util.CheatSheet.java
com.google.android.apps.muzei.util.DrawInsetsFrameLayout.java
com.google.android.apps.muzei.util.IOUtil.java
com.google.android.apps.muzei.util.ImageBlurrer.java
com.google.android.apps.muzei.util.LogUtil.java
com.google.android.apps.muzei.util.LogoPaths.java
com.google.android.apps.muzei.util.MathUtil.java
com.google.android.apps.muzei.util.MultiSelectionController.java
com.google.android.apps.muzei.util.ObservableHorizontalScrollView.java
com.google.android.apps.muzei.util.PanScaleProxyView.java
com.google.android.apps.muzei.util.PanView.java
com.google.android.apps.muzei.util.ScrimUtil.java
com.google.android.apps.muzei.util.Scrollbar.java
com.google.android.apps.muzei.util.SelectionBuilder.java
com.google.android.apps.muzei.util.ShadowDipsTextView.java
com.google.android.apps.muzei.util.SvgPathParser.java
com.google.android.apps.muzei.util.TickingFloatAnimator.java
com.google.android.apps.muzei.util.TypefaceUtil.java
com.google.android.apps.muzei.util.Zoomer.java
net.rbgrn.android.glwallpaperservice.BaseConfigChooser.java
net.rbgrn.android.glwallpaperservice.GLWallpaperService.java