package com.netease.nim.uikit.common.ui.imageview; /* * Copyright 2012 Laurence Dawson * * 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. */ /* * This class is based upon the file ImageViewTouchBase.java which can be found at: * https://dl-ssl.google.com/dl/googlesource/git-repo/repo * * Copyright (C) 2009 The Android Open Source Project */ import com.netease.nim.uikit.common.util.media.SampleSizeUtil; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Rect; import android.os.Build; import android.os.SystemClock; import android.support.v4.view.ViewPager; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.animation.Animation; import android.view.animation.TranslateAnimation; public abstract class BaseZoomableImageView extends View { // Statics static final float sPanRate = 7; static final float sScaleRate = 1.25F; static final int sPaintDelay = 250; static final int sAnimationDelay = 500; public static final int MIN_SDK_ENABLE_LAYER_TYPE_HARDWARE = Build.VERSION_CODES.ICE_CREAM_SANDWICH; // 横屏时特殊处理,但高大于宽的2倍时,就以宽充满 private static final float MAX_IMAGE_RATIO_WIDTH_LARGE_LANDSCAPE = 2F; private static final float MAX_IMAGE_RATIO_LARGE = 5F; // This is the base transformation which is used to show the image // initially. The current computation for this shows the image in // it's entirety, letterboxing as needed. One could chose to // show the image as cropped instead. // // This matrix is recomputed when we go from the thumbnail image to // the full size image. private Matrix mBaseMatrix = new Matrix(); // This is the supplementary transformation which reflects what // the user has done in terms of zooming and panning. // // This matrix remains the same when we go from the thumbnail image // to the full size image. private Matrix mSuppMatrix = new Matrix(); // This is the final matrix which is computed as the concatentation // of the base matrix and the supplementary matrix. private Matrix mDisplayMatrix = new Matrix(); // A replacement ImageView matrix private Matrix mMatrix = new Matrix(); // Used to filter the bitmaps when hardware acceleration is not enabled private Paint mPaint; // Temporary buffer used for getting the values out of a matrix. private float[] mMatrixValues = new float[9]; // Dimensions for the view private int mThisWidth = -1, mThisHeight = -1; // The max zoom for the view, determined programatically private float mMaxZoom; // If not null, calls setImageBitmap when onLayout is triggered private Runnable mOnLayoutRunnable = null; // Stacked to the internal queue to invalidate the view private Runnable mRefresh = null; // The time of the last draw operation private double mLastDraw = 0; // The current bitmap being displayed. protected Bitmap mBitmap; // Stacked to the internal queue to scroll the view private Runnable mFling = null; private boolean fling = false; // Single tap listener protected ImageGestureListener mImageGestureListener; protected ViewPager mViewPager; private boolean landscape = false; // Programatic entry point public BaseZoomableImageView(Context context) { super(context); initBaseZoomableImageView( context ); } // XML entry point public BaseZoomableImageView(Context context, AttributeSet attrs) { super(context, attrs); initBaseZoomableImageView( context ); } // Setup the view @SuppressLint("NewApi") protected void initBaseZoomableImageView( Context context) { mPaint = new Paint(); mPaint.setDither(true); mPaint.setFilterBitmap(true); mPaint.setAntiAlias(true); if(context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE){ landscape = true; }else { landscape = false; } // Setup the refresh runnable mRefresh = new Runnable() { @Override public void run() { postInvalidate(); } }; } // Set the single tap listener public void setImageGestureListener( ImageGestureListener listener ){ this.mImageGestureListener = listener; } public void setViewPager(ViewPager viewPager) { this.mViewPager = viewPager; } // Get the bitmap for the view public Bitmap getImageBitmap(){ return mBitmap; } // Free the bitmaps and matrices public void clear(){ if(mBitmap!=null && !mBitmap.isRecycled()) { mBitmap.recycle(); } mBitmap = null; } // When the layout is calculated, set the @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); mThisWidth = right - left; mThisHeight = bottom - top; Runnable r = mOnLayoutRunnable; if (r != null) { mOnLayoutRunnable = null; r.run(); } // if (mBitmap != null) { // setBaseMatrix(mBitmap, mBaseMatrix); // setImageMatrix(getImageViewMatrix()); // } } // Translate a given point through a given matrix. static protected void translatePoint(Matrix matrix, float [] xy) { matrix.mapPoints(xy); } // Identical to the setImageMatrix method in ImageView public void setImageMatrix(Matrix m){ if (m != null && m.isIdentity()) { m = null; } // don't invalidate unless we're actually changing our matrix if (m == null && !this.mMatrix.isIdentity() || m != null && !this.mMatrix.equals(m)) { this.mMatrix.set(m); invalidate(); } } // Sets the bitmap for the image and resets the base public void setImageBitmap(final Bitmap bitmap) { setImageBitmap(bitmap, true); } // Sets the bitmap for the image and resets the base @SuppressLint("NewApi") public void setImageBitmap(final Bitmap bitmap, final boolean fitScreen) { //版本过低或者长度大于最大纹理限制,不采用硬件加速 if (Build.VERSION.SDK_INT >= MIN_SDK_ENABLE_LAYER_TYPE_HARDWARE) { if (bitmap != null && (bitmap.getHeight() > SampleSizeUtil.getTextureSize() || bitmap.getWidth() > SampleSizeUtil.getTextureSize())) { setLayerType(View.LAYER_TYPE_SOFTWARE, null); } else { setLayerType(View.LAYER_TYPE_HARDWARE, null); } } final int viewWidth = getWidth(); if (viewWidth <= 0) { mOnLayoutRunnable = new Runnable() { public void run() { setImageBitmap(bitmap, fitScreen); } }; return; } Bitmap oldBitmap = this.mBitmap; if (bitmap != null) { setBaseMatrix(bitmap, mBaseMatrix); this.mBitmap = bitmap; } else { mBaseMatrix.reset(); this.mBitmap = bitmap; } if (oldBitmap != null && oldBitmap != mBitmap && !oldBitmap.isRecycled()) { oldBitmap.recycle(); } mSuppMatrix.reset(); setImageMatrix(getImageViewMatrix()); mMaxZoom = maxZoom(); // Set the image to fit the screen if(fitScreen) { zoomToScreen(); } } /** * * Sets the bitmap for the image and resets the base * @date 2014-4-29 * @param bitmap * @param selection */ public void setImageBitmap(final Bitmap bitmap,final Rect selection ) { final int viewWidth = getWidth(); if (viewWidth <= 0) { mOnLayoutRunnable = new Runnable() { public void run() { setImageBitmap(bitmap, updateSelection()); } }; return; } Bitmap oldBitmap = this.mBitmap; if (bitmap != null) { setBaseMatrix(bitmap, mBaseMatrix,selection ); this.mBitmap = bitmap; } else { mBaseMatrix.reset(); this.mBitmap = bitmap; } if (oldBitmap != null && !oldBitmap.isRecycled()) { oldBitmap.recycle(); } mSuppMatrix.reset(); setImageMatrix(getImageViewMatrix()); mMaxZoom = maxZoom(); } // Unchanged from ImageViewTouchBase // Center as much as possible in one or both axis. Centering is // defined as follows: if the image is scaled down below the // view's dimensions then center it (literally). If the image // is scaled larger than the view and is translated out of view // then translate it back into view (i.e. eliminate black bars). protected void center(boolean vertical, boolean horizontal, boolean animate) { if (mBitmap == null) return; Matrix m = getImageViewMatrix(); float [] topLeft = new float[] { 0, 0 }; float [] botRight = new float[] { mBitmap.getWidth(), mBitmap.getHeight() }; translatePoint(m, topLeft); translatePoint(m, botRight); float height = botRight[1] - topLeft[1]; float width = botRight[0] - topLeft[0]; float deltaX = 0, deltaY = 0; if (vertical) { int viewHeight = getHeight(); if (height < viewHeight) { deltaY = (viewHeight - height)/2 - topLeft[1]; } else if (topLeft[1] > 0) { deltaY = -topLeft[1]; } else if (botRight[1] < viewHeight) { deltaY = getHeight() - botRight[1]; } } if (horizontal) { int viewWidth = getWidth(); if (width < viewWidth) { deltaX = (viewWidth - width)/2 - topLeft[0]; } else if (topLeft[0] > 0) { deltaX = -topLeft[0]; } else if (botRight[0] < viewWidth) { deltaX = viewWidth - botRight[0]; } } postTranslate(deltaX, deltaY); if (animate) { Animation a = new TranslateAnimation(-deltaX, 0, -deltaY, 0); a.setStartTime(SystemClock.elapsedRealtime()); a.setDuration(250); setAnimation(a); } setImageMatrix(getImageViewMatrix()); } // Unchanged from ImageViewTouchBase protected float getValue(Matrix matrix, int whichValue) { matrix.getValues(mMatrixValues); return mMatrixValues[whichValue]; } // Get the scale factor out of the matrix. protected float getScale(Matrix matrix) { // If the bitmap is set return the scale if(mBitmap!=null) return getValue(matrix, Matrix.MSCALE_X); // Otherwise return the default value of 1 else return 1f; } // Returns the current scale of the view public float getScale() { return getScale(mSuppMatrix); } // Setup the base matrix so that the image is centered and scaled properly. private void setBaseMatrix(Bitmap bitmap, Matrix matrix) { float viewWidth = getWidth(); float viewHeight = getHeight(); matrix.reset(); float widthScale = Math.min(viewWidth / (float)bitmap.getWidth(), 1.0f); float heightScale = Math.min(viewHeight / (float)bitmap.getHeight(), 1.0f); float scale; if (widthScale > heightScale) { scale = heightScale; } else { scale = widthScale; } matrix.setScale(scale, scale); matrix.postTranslate( (viewWidth - ((float)bitmap.getWidth() * scale))/2F, (viewHeight - ((float)bitmap.getHeight() * scale))/2F); } /** * Setup the base matrix so that the image is centered and scaled properly. * 根据Bitmap和Rect,设置初始的显示矩阵Matrix * @author Linleja * @date 2014-4-29 * @param bitmap * @param matrix * @param selection */ private void setBaseMatrix(Bitmap bitmap, Matrix matrix,Rect selection) { if(selection == null){ return ; } float viewWidth = selection.right - selection.left; float viewHeight = selection.bottom - selection.top; matrix.reset(); float widthRatio = viewWidth / (float)bitmap.getWidth(); float heighRatio = viewHeight / (float)bitmap.getHeight(); float scale = 1.0f; if (widthRatio > heighRatio) { scale = widthRatio; } else { scale = heighRatio; } matrix.setScale(scale, scale); matrix.postTranslate( ((getWidth() - (float)bitmap.getWidth() * scale))/2F, ((getHeight() - (float)bitmap.getHeight() * scale))/2F); } // Combine the base matrix and the supp matrix to make the final matrix. protected Matrix getImageViewMatrix() { mDisplayMatrix.set(mBaseMatrix); mDisplayMatrix.postConcat(mSuppMatrix); return mDisplayMatrix; } // Sets the maximum zoom, which is a scale relative to the base matrix. It is calculated to show // the image at 400% zoom regardless of screen or image orientation. If in the future we decode // the full 3 megapixel image, rather than the current 1024x768, this should be changed down to // 200%. protected float maxZoom() { if (mBitmap == null) return 1F; float fw = (float) mBitmap.getWidth() / (float)mThisWidth; float fh = (float) mBitmap.getHeight() / (float)mThisHeight; float max = Math.max(fw, fh) * 16; //设置放大的下限 if(max < 1F){ max = 1F; } return max; } // Tries to make best use of the space by zooming the picture public float zoomDefault() { if (mBitmap == null) return 1F; float fw = (float)mThisWidth/(float)mBitmap.getWidth(); float fh = (float)mThisHeight/(float)mBitmap.getHeight(); return Math.max(Math.min(fw, fh),1); } // Unchanged from ImageViewTouchBase protected void zoomTo(float scale, float centerX, float centerY) { if (scale > mMaxZoom) { scale = mMaxZoom; } float oldScale = getScale(); float deltaScale = scale / oldScale; mSuppMatrix.postScale(deltaScale, deltaScale, centerX, centerY); setImageMatrix(getImageViewMatrix()); center(true, true, false); } // Unchanged from ImageViewTouchBase protected void zoomTo(final float scale, final float centerX, final float centerY, final float durationMs) { final float incrementPerMs = (scale - getScale()) / durationMs; final float oldScale = getScale(); final long startTime = System.currentTimeMillis(); // Setup the zoom runnable post(new Runnable() { public void run() { long now = System.currentTimeMillis(); float currentMs = Math.min(durationMs, (float)(now - startTime)); float target = oldScale + (incrementPerMs * currentMs); zoomTo(target, centerX, centerY); if (currentMs < durationMs) { post(this); } } }); } private boolean adjustLongImageEnable = true; public void setAdjustLongImageEnable(boolean enable) { this.adjustLongImageEnable = enable; } public void zoomToScreen() { if (mBitmap == null) return; float scale = 1f; float fw = (float)mThisWidth/(float)mBitmap.getWidth(); boolean needAdjust = false; if (adjustLongImageEnable) { //长>>>宽 比如长微博 横向撑满屏幕,上下滑动浏览全文 if ((float)mBitmap.getHeight() / (float)mBitmap.getWidth() > MAX_IMAGE_RATIO_LARGE) { needAdjust = true; scale = fw; } else if (landscape && (float)mBitmap.getHeight() / (float)mBitmap.getWidth() > MAX_IMAGE_RATIO_WIDTH_LARGE_LANDSCAPE) { needAdjust = true; scale = fw; } } if (needAdjust) { float oldScale = getScale(); float deltaScale = scale / oldScale; mBaseMatrix.reset(); mSuppMatrix.postScale(deltaScale, deltaScale, 0, 0); setImageMatrix(getImageViewMatrix()); } else { zoomTo(zoomDefault()); } } // Unchanged from ImageViewTouchBase public void zoomTo(float scale) { float width = getWidth(); float height = getHeight(); zoomTo(scale, width/2F, height/2F); } // Unchanged from ImageViewTouchBase public void zoomIn() { zoomIn(sScaleRate); } // Unchanged from ImageViewTouchBase public void zoomOut() { zoomOut(sScaleRate); } // Unchanged from ImageViewTouchBase protected void zoomIn(float rate) { if (getScale() >= mMaxZoom) { return; // Don't let the user zoom into the molecular level. } if (mBitmap == null) { return; } float width = getWidth(); float height = getHeight(); mSuppMatrix.postScale(rate, rate, width/2F, height/2F); setImageMatrix(getImageViewMatrix()); } // Unchanged from ImageViewTouchBase protected void zoomOut(float rate) { if (mBitmap == null) { return; } float width = getWidth(); float height = getHeight(); Matrix tmp = new Matrix(mSuppMatrix); tmp.postScale(1F/sScaleRate, 1F/sScaleRate, width/2F, height/2F); if (getScale(tmp) < 1F) { mSuppMatrix.setScale(1F, 1F, width/2F, height/2F); } else { mSuppMatrix.postScale(1F/rate, 1F/rate, width/2F, height/2F); } setImageMatrix(getImageViewMatrix()); center(true, true, false); } // Unchanged from ImageViewTouchBase protected boolean postTranslate(float dx, float dy) { return mSuppMatrix.postTranslate(dx, dy); } // Fling a view by a distance over time protected void scrollBy( float distanceX, float distanceY, final float durationMs ) { final float dx = distanceX; final float dy = distanceY; final long startTime = System.currentTimeMillis(); mFling = new Runnable() { float old_x = 0; float old_y = 0; public void run() { long now = System.currentTimeMillis(); float currentMs = Math.min( durationMs, now - startTime ); float x = easeOut( currentMs, 0, dx, durationMs ); float y = easeOut( currentMs, 0, dy, durationMs ); postTranslate( ( x - old_x ), ( y - old_y ) ); center(true, true, false); old_x = x; old_y = y; if ( currentMs < durationMs ) { fling = post( this ); } else { stopFling(); } } }; fling = post( mFling ); } protected void stopFling() { removeCallbacks(mFling); if (fling) { fling = false; onScrollFinish(); } } protected boolean fling() { return fling; } // Gradually slows down a fling velocity private float easeOut( float time, float start, float end, float duration){ return end * ( ( time = time / duration - 1 ) * time * time + 1 ) + start; } protected void onScrollFinish() { } // Custom draw operation to draw the bitmap using mMatrix @SuppressLint("NewApi") @Override protected void onDraw(Canvas canvas) { // Check if the bitmap was ever set if(mBitmap!=null && !mBitmap.isRecycled() ){ // If the current version is above Gingerbread and the layer type is // hardware accelerated, the paint is no longer needed if( Build.VERSION.SDK_INT >= MIN_SDK_ENABLE_LAYER_TYPE_HARDWARE && getLayerType() == View.LAYER_TYPE_HARDWARE ){ canvas.drawBitmap(mBitmap, mMatrix, null); } else { // Check if the time between draws has been met and draw the bitmap if( (System.currentTimeMillis()-mLastDraw) > sPaintDelay ){ canvas.drawBitmap(mBitmap, mMatrix, mPaint); mLastDraw = System.currentTimeMillis(); } // Otherwise draw the bitmap without the paint and resubmit a new request else{ canvas.drawBitmap(mBitmap, mMatrix, null); removeCallbacks(mRefresh); postDelayed(mRefresh, sPaintDelay); } } } } protected boolean isScrollOver(float distanceX) { try { if (mDisplayMatrix != null) { float m_x = getValue(mDisplayMatrix, Matrix.MTRANS_X); //图片的左边离屏宽度 float width = getWidth() - m_x; //width 代表 屏幕宽度+左边离屏宽度 //mBitmap.getWidth() * getValue(mDisplayMatrix, Matrix.MSCALE_X) 代表 当前图片显示宽度 //width == mBitmap.getWidth() * getValue(mDisplayMatrix, Matrix.MSCALE_X) 意味着图片右边离屏宽度 == 0,已经滑到最右边 if ((m_x == 0 && distanceX <= 0) //到达图片的最左边继续往左边滑 || (width == mBitmap.getWidth() //到达图片的最右边继续往右边滑 * getValue(mDisplayMatrix, Matrix.MSCALE_X) && distanceX >= 0)) { System.out.println("ScrollOver"); return true; } } } catch (IllegalArgumentException e) { Log.v("Vincent", "isScrollOver"); e.printStackTrace(); } return false; } protected Rect updateSelection() { return null; } }