Android PhotoView简单应用

PhotoView是一个图片浏览器,支持多点触摸来进行图片的滚动和缩放。

简单使用

build.gradle中添加依赖

implementation 'com.github.chrisbanes:PhotoView:2.0.0'

repositories里面添加库

maven { url 'https://www.jitpack.io' }

布局文件引用PhotoView

<com.github.chrisbanes.photoview.PhotoView
    android:id="@+id/photo_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

PhotoView提供了几种添加图片的方式

public void setImageDrawable(Drawable drawable)
public void setImageResource(int resId)
public void setImageURI(Uri uri)

PhotoView主要方法

// 设置缩放值,默认是1.0,1.75,3.0
public void setMinimumScale(float minimumScale)
public void setMediumScale(float mediumScale)
public void setMaximumScale(float maximumScale)
public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale)
public void setScale(float scale)
public void setScale(float scale, boolean animate)
public void setScale(float scale, float focalX, float focalY, boolean animate)

public float getMinimumScale()
public float getMediumScale()
public float getMaximumScale()
public float getScale()

// 监听事件
public void setOnPhotoTapListener(OnPhotoTapListener listener)
public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener listener)
public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener)
public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangedListener)
public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener)

github地址: https://github.com/chrisbanes/PhotoView

代码解析

PhotoView继承ImageView,并通过PhotoViewAttacher来处理事务

private void init() {
    attacher = new PhotoViewAttacher(this);
    //We always pose as a Matrix scale type, though we can change to another scale type
    //via the attacher
    super.setScaleType(ScaleType.MATRIX);
}

public PhotoViewAttacher getAttacher() {
    return attacher;
}

@Override
public ScaleType getScaleType() {
    return attacher.getScaleType();
}

@Override
public Matrix getImageMatrix() {
    return attacher.getImageMatrix();
}

@Override
public void setOnLongClickListener(OnLongClickListener l) {
    attacher.setOnLongClickListener(l);
}

@Override
public void setOnClickListener(OnClickListener l) {
    attacher.setOnClickListener(l);
}

@Override
public void setScaleType(ScaleType scaleType) {
    if (attacher != null) {
        attacher.setScaleType(scaleType);
    }
}

 ... ...

PhotoViewAttacher类主要干两件事,

  • 处理界面手势,包括单击、双击、长按,滑动,缩放等
  • ImageView进行位移、缩放、旋转的操作
public PhotoViewAttacher(ImageView imageView) {
    mImageView = imageView;
    // 捕捉手势操作
    imageView.setOnTouchListener(this);
    // 监听布局变化
    imageView.addOnLayoutChangeListener(this);

    if (imageView.isInEditMode()) {
        return;
    }

    mBaseRotation = 0.0f;

    // Create Gesture Detectors...
    mScaleDragDetector = new CustomGestureDetector(imageView.getContext(), this);

    // GestureDetector捕捉ImageView长按和滑动
    mGestureDetector = new GestureDetector(imageView.getContext(), new GestureDetector.SimpleOnGestureListener() {

        // forward long click listener
        @Override
        public void onLongPress(MotionEvent e) {
            if (mLongClickListener != null) {
                mLongClickListener.onLongClick(mImageView);
            }
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2,
                               float velocityX, float velocityY) {
            if (mSingleFlingListener != null) {
                if (getScale() > DEFAULT_MIN_SCALE) {
                    return false;
                }

                if (MotionEventCompat.getPointerCount(e1) > SINGLE_TOUCH
                        || MotionEventCompat.getPointerCount(e2) > SINGLE_TOUCH) {
                    return false;
                }

                return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY);
            }
            return false;
        }
    });

    // GestureDetector捕捉ImageView单击和双击操作
    mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            // 单击操作
            if (mOnClickListener != null) {
                mOnClickListener.onClick(mImageView);
            }
            final RectF displayRect = getDisplayRect();

            if (displayRect != null) {
                final float x = e.getX(), y = e.getY();

                // Check to see if the user tapped on the photo
                if (displayRect.contains(x, y)) {

                    float xResult = (x - displayRect.left)
                            / displayRect.width();
                    float yResult = (y - displayRect.top)
                            / displayRect.height();

                    if (mPhotoTapListener != null) {
                        mPhotoTapListener.onPhotoTap(mImageView, xResult, yResult);
                    }
                    return true;
                } else {
                    if (mOutsidePhotoTapListener != null) {
                        mOutsidePhotoTapListener.onOutsidePhotoTap(mImageView);
                    }
                }
            }
            return false;
        }

        @Override
        public boolean onDoubleTap(MotionEvent ev) {
            // 双击操作,图片变大
            try {
                float scale = getScale();
                float x = ev.getX();
                float y = ev.getY();

                if (scale < getMediumScale()) {
                    setScale(getMediumScale(), x, y, true);
                } else if (scale >= getMediumScale() && scale < getMaximumScale()) {
                    setScale(getMaximumScale(), x, y, true);
                } else {
                    setScale(getMinimumScale(), x, y, true);
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                // Can sometimes happen when getX() and getY() is called
            }

            return true;
        }

        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            // Wait for the confirmed onDoubleTap() instead
            return false;
        }
    });
}

onTouch方法,mScaleDragDetectormGestureDetector会对手势做出处理

@Override
public boolean onTouch(View v, MotionEvent ev) {
    boolean handled = false;

    if (mZoomEnabled && Util.hasDrawable((ImageView) v)) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                ViewParent parent = v.getParent();
                // First, disable the Parent from intercepting the touch
                // event
                if (parent != null) {
                    parent.requestDisallowInterceptTouchEvent(true);
                }

                // If we're flinging, and the user presses down, cancel
                // fling
                cancelFling();
                break;

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                // If the user has zoomed less than min scale, zoom back
                // to min scale
                if (getScale() < mMinScale) {
                    RectF rect = getDisplayRect();
                    if (rect != null) {
                        v.post(new AnimatedZoomRunnable(getScale(), mMinScale,
                                rect.centerX(), rect.centerY()));
                        handled = true;
                    }
                }
                break;
        }

        // Try the Scale/Drag detector
        if (mScaleDragDetector != null) {
            boolean wasScaling = mScaleDragDetector.isScaling();
            boolean wasDragging = mScaleDragDetector.isDragging();

            // mScaleDragDetector处理缩放
            handled = mScaleDragDetector.onTouchEvent(ev);

            boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling();
            boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging();

            mBlockParentIntercept = didntScale && didntDrag;
        }

        // Check to see if the user double tapped
        if (mGestureDetector != null && mGestureDetector.onTouchEvent(ev)) {
            handled = true;
        }

    }

    return handled;
}

// PhotoViewAttacher继承OnGestureListener,处理拖拽、缩放事件
@Override
public void onDrag(float dx, float dy) {
    if (mScaleDragDetector.isScaling()) {
        return; // Do not drag if we are already scaling
    }

    mSuppMatrix.postTranslate(dx, dy);
    checkAndDisplayMatrix();

    /*
     * Here we decide whether to let the ImageView's parent to start taking
     * over the touch event.
     *
     * First we check whether this function is enabled. We never want the
     * parent to take over if we're scaling. We then check the edge we're
     * on, and the direction of the scroll (i.e. if we're pulling against
     * the edge, aka 'overscrolling', let the parent take over).
     */
    // ViewPager和ScrollView等边界问题
    ViewParent parent = mImageView.getParent();
    if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) {
        if (mScrollEdge == EDGE_BOTH
                || (mScrollEdge == EDGE_LEFT && dx >= 1f)
                || (mScrollEdge == EDGE_RIGHT && dx <= -1f)) {
            if (parent != null) {
                parent.requestDisallowInterceptTouchEvent(false);
            }
        }
    } else {
        if (parent != null) {
            parent.requestDisallowInterceptTouchEvent(true);
        }
    }
}

@Override
public void onFling(float startX, float startY, float velocityX,
                    float velocityY) {
    mCurrentFlingRunnable = new FlingRunnable(mImageView.getContext());
    mCurrentFlingRunnable.fling(getImageViewWidth(mImageView),
            getImageViewHeight(mImageView), (int) velocityX, (int) velocityY);
    mImageView.post(mCurrentFlingRunnable);
}

@Override
public void onScale(float scaleFactor, float focusX, float focusY) {
    if ((getScale() < mMaxScale || scaleFactor < 1f) && (getScale() > mMinScale || scaleFactor > 1f)) {
        if (mScaleChangeListener != null) {
            mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY);
        }
        mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
        checkAndDisplayMatrix();
    }
}

FlingRunnable实现滑动操作

private class FlingRunnable implements Runnable {

    private final OverScroller mScroller;
    private int mCurrentX, mCurrentY;

    public FlingRunnable(Context context) {
        mScroller = new OverScroller(context);
    }

    public void cancelFling() {
        mScroller.forceFinished(true);
    }

    public void fling(int viewWidth, int viewHeight, int velocityX,
                      int velocityY) {
        final RectF rect = getDisplayRect();
        if (rect == null) {
            return;
        }

        final int startX = Math.round(-rect.left);
        final int minX, maxX, minY, maxY;

        if (viewWidth < rect.width()) {
            minX = 0;
            maxX = Math.round(rect.width() - viewWidth);
        } else {
            minX = maxX = startX;
        }

        final int startY = Math.round(-rect.top);
        if (viewHeight < rect.height()) {
            minY = 0;
            maxY = Math.round(rect.height() - viewHeight);
        } else {
            minY = maxY = startY;
        }

        mCurrentX = startX;
        mCurrentY = startY;

        // If we actually can move, fling the scroller
        if (startX != maxX || startY != maxY) {
            mScroller.fling(startX, startY, velocityX, velocityY, minX,
                    maxX, minY, maxY, 0, 0);
        }
    }

    @Override
    public void run() {
        if (mScroller.isFinished()) {
            return; // remaining post that should not be handled
        }

        if (mScroller.computeScrollOffset()) {

            final int newX = mScroller.getCurrX();
            final int newY = mScroller.getCurrY();

            mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY);
            setImageViewMatrix(getDrawMatrix());

            mCurrentX = newX;
            mCurrentY = newY;

            // Post On animation
            Compat.postOnAnimation(mImageView, this);
        }
    }
}

PhotoViewAttacher里面有mBaseMatrixmDrawMatrixmSuppMatrix,用来记录ImageView的转换。其中mBaseMatrix用来记录最初的位置,mSuppMatrix用来记录手势操作的位置。

@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
    // Update our base matrix, as the bounds have changed
    updateBaseMatrix(mImageView.getDrawable());
}

/**
 * Calculate Matrix for FIT_CENTER
 *
 * @param drawable - Drawable being displayed
 */
private void updateBaseMatrix(Drawable drawable) {
    if (drawable == null) {
        return;
    }

    final float viewWidth = getImageViewWidth(mImageView);
    final float viewHeight = getImageViewHeight(mImageView);
    final int drawableWidth = drawable.getIntrinsicWidth();
    final int drawableHeight = drawable.getIntrinsicHeight();

    mBaseMatrix.reset();

    final float widthScale = viewWidth / drawableWidth;
    final float heightScale = viewHeight / drawableHeight;

    if (mScaleType == ScaleType.CENTER) {
        mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F,
                (viewHeight - drawableHeight) / 2F);

    } else if (mScaleType == ScaleType.CENTER_CROP) {
        float scale = Math.max(widthScale, heightScale);
        mBaseMatrix.postScale(scale, scale);
        mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
                (viewHeight - drawableHeight * scale) / 2F);

    } else if (mScaleType == ScaleType.CENTER_INSIDE) {
        float scale = Math.min(1.0f, Math.min(widthScale, heightScale));
        mBaseMatrix.postScale(scale, scale);
        mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
                (viewHeight - drawableHeight * scale) / 2F);

    } else {
        RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight);
        RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight);

        if ((int) mBaseRotation % 180 != 0) {
            mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth);
        }

        switch (mScaleType) {
            case FIT_CENTER:
                mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER);
                break;

            case FIT_START:
                mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START);
                break;

            case FIT_END:
                mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END);
                break;

            case FIT_XY:
                mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL);
                break;

            default:
                break;
        }
    }

    resetMatrix();
}

/**
 * Resets the Matrix back to FIT_CENTER, and then displays its contents
 */
private void resetMatrix() {
    mSuppMatrix.reset();
    setRotationBy(mBaseRotation);
    setImageViewMatrix(getDrawMatrix());
    checkMatrixBounds();
}

// mDrawMatrix是mBaseMatrix和mSuppMatrix的组合
private Matrix getDrawMatrix() {
    mDrawMatrix.set(mBaseMatrix);
    mDrawMatrix.postConcat(mSuppMatrix);
    return mDrawMatrix;
}

// 校验边界
private boolean checkMatrixBounds() {
    // rect是实际图片的位置
    final RectF rect = getDisplayRect(getDrawMatrix());
    if (rect == null) {
        return false;
    }

    final float height = rect.height(), width = rect.width();
    float deltaX = 0, deltaY = 0;

    // 1. 如果图片的高度小于ImageView的高度,根据mScaleType的值调整位置
    // 2. 如果图片的高度大于ImageView的高度,图片top不能大于0,bottom不能小于viewHeight
    final int viewHeight = getImageViewHeight(mImageView);
    if (height <= viewHeight) {
        switch (mScaleType) {
            case FIT_START:
                deltaY = -rect.top;
                break;
            case FIT_END:
                deltaY = viewHeight - height - rect.top;
                break;
            default:
                deltaY = (viewHeight - height) / 2 - rect.top;
                break;
        }
    } else if (rect.top > 0) {
        deltaY = -rect.top;
    } else if (rect.bottom < viewHeight) {
        deltaY = viewHeight - rect.bottom;
    }

    // 1. 如果图片的宽度小于ImageView的宽度,根据mScaleType的值调整位置
    // 2. 如果图片的宽度大于ImageView的宽度,图片left不能大于0,right不能小于viewWidth
    final int viewWidth = getImageViewWidth(mImageView);
    if (width <= viewWidth) {
        switch (mScaleType) {
            case FIT_START:
                deltaX = -rect.left;
                break;
            case FIT_END:
                deltaX = viewWidth - width - rect.left;
                break;
            default:
                deltaX = (viewWidth - width) / 2 - rect.left;
                break;
        }
        mScrollEdge = EDGE_BOTH;
    } else if (rect.left > 0) {
        mScrollEdge = EDGE_LEFT;
        deltaX = -rect.left;
    } else if (rect.right < viewWidth) {
        deltaX = viewWidth - rect.right;
        mScrollEdge = EDGE_RIGHT;
    } else {
        mScrollEdge = EDGE_NONE;
    }

    // Finally actually translate the matrix
    mSuppMatrix.postTranslate(deltaX, deltaY);
    return true;
}

private int getImageViewWidth(ImageView imageView) {
    return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight();
}

private int getImageViewHeight(ImageView imageView) {
    return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom();
}

// 对ImageView进行位置转化,返回转换后的Rect
private RectF getDisplayRect(Matrix matrix) {
    Drawable d = mImageView.getDrawable();
    if (d != null) {
        mDisplayRect.set(0, 0, d.getIntrinsicWidth(),
                d.getIntrinsicHeight());
        matrix.mapRect(mDisplayRect);
        return mDisplayRect;
    }
    return null;
}

/**
 * Helper method that simply checks the Matrix, and then displays the result
 */
private void checkAndDisplayMatrix() {
    if (checkMatrixBounds()) {
        setImageViewMatrix(getDrawMatrix());
    }
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要实现这个功能,你可以使用以下步骤: 1. 在你的项目中添加PhotoView库,可以使用以下Gradle依赖: ```gradle implementation 'com.github.chrisbanes:PhotoView:2.3.0' ``` 2. 创建一个自定义的PhotoView类,并继承自PhotoView类,并实现视频播放的功能。你可以使用Android系统自带的VideoView或者其他第三方库来实现视频播放。 ```java public class CustomPhotoView extends PhotoView { private VideoView mVideoView; public CustomPhotoView(Context context) { super(context); init(); } public CustomPhotoView(Context context, AttributeSet attr) { super(context, attr); init(); } public CustomPhotoView(Context context, AttributeSet attr, int defStyle) { super(context, attr, defStyle); init(); } private void init() { mVideoView = new VideoView(getContext()); mVideoView.setMediaController(new MediaController(getContext())); mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mediaPlayer) { mediaPlayer.setLooping(true); } }); } public void setVideoURI(Uri uri) { mVideoView.setVideoURI(uri); } public void startVideo() { mVideoView.start(); } public void stopVideo() { mVideoView.stopPlayback(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); stopVideo(); } } ``` 3. 在布局文件中使用自定义的PhotoView,并设置相应的属性和视频URI。 ```xml <com.example.CustomPhotoView android:id="@+id/photo_view" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="fitCenter" app:src="@drawable/photo" app:video_uri="@raw/video" /> ``` 4. 在自定义的PhotoView类中处理触摸事件,实现上下滑动的效果。 ```java public class CustomPhotoView extends PhotoView { private static final int INVALID_POINTER_ID = -1; private float mLastTouchX; private float mLastTouchY; private int mActivePointerId = INVALID_POINTER_ID; public CustomPhotoView(Context context) { super(context); init(); } public CustomPhotoView(Context context, AttributeSet attr) { super(context, attr); init(); } public CustomPhotoView(Context context, AttributeSet attr, int defStyle) { super(context, attr, defStyle); init(); } private void init() { setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent event) { final int action = event.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { final float x = event.getX(); final float y = event.getY(); mLastTouchX = x; mLastTouchY = y; mActivePointerId = event.getPointerId(0); break; } case MotionEvent.ACTION_MOVE: { final int pointerIndex = event.findPointerIndex(mActivePointerId); final float x = event.getX(pointerIndex); final float y = event.getY(pointerIndex); final float dx = x - mLastTouchX; final float dy = y - mLastTouchY; mLastTouchX = x; mLastTouchY = y; // 上下滑动的阈值 final int SCROLL_THRESHOLD = 10; if (Math.abs(dy) > SCROLL_THRESHOLD) { getParent().requestDisallowInterceptTouchEvent(true); } break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { mActivePointerId = INVALID_POINTER_ID; break; } case MotionEvent.ACTION_POINTER_UP: { final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; final int pointerId = event.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mLastTouchX = event.getX(newPointerIndex); mLastTouchY = event.getY(newPointerIndex); mActivePointerId = event.getPointerId(newPointerIndex); } break; } } return false; } }); } } ``` 这样,你就可以使用自定义的PhotoView来支持上下滑动和视频播放了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值