Android類似微信圖片查看下拉縮放并移動
平時做開發(fā)的時候圖片查看肯定是經常碰到的一個功能,在做社交App或內容瀏覽App的時候,圖片查看的關閉方式做的人性化一點,肯定會給用戶留下正面的印象。這里是仿照微信的圖片關閉方式擼的代碼,提供一點思路。
首先上一張效果圖:

再上一張微信效果圖對比:

觀察上面兩張圖,我們總結出這個圖片手勢關閉的特點:
手指下拉圖片,圖片會被移動并且縮小,同時背景也有個透明度的變化;
正常狀態(tài)按下,往上移動,是不能對圖片進行移動的,但是往下拉一下之后就可以上下左右隨便移動;
要實現上面的效果,我們需要操作圖片查看Activity中的ViewPager的事件獲取,所以這里自定義一個ScaleViewPager繼承系統(tǒng)的ViewPager。并且把該viewpager的背景設置成黑色,一般圖片查看背景都是黑色的。
public class ScaleViewPager extends ViewPager {public ScaleViewPager(Context context) {super(context);init(context);}public ScaleViewPager(Context context, AttributeSet attrs) {super(context, attrs);init(context);}public void init(Context context){setBackgroundColor(Color.BLACK);}}
當然,必不可少的是給圖片查看Activity設置透明背景Theme:
<style name="ImageBrowseTransitionTheme" parent="AppTheme"><item name="android:windowIsTranslucent">true</item><item name="android:windowBackground">@android:color/transparent</item><item name="android:windowTranslucentStatus">true</item><item name="android:windowActivityTransitions">true</item><item name="android:windowSharedElementEnterTransition">@transition/changebounds</item><item name="android:windowSharedElementReturnTransition">@transition/changebounds</item></style>
下拉關閉圖片的操作,這個MotionEvent是全屏都可以接收到的,而圖片的縮放與移動,只是顯示的圖片自己的縮放與移動,所以我們要重寫ViewPager的 OnTouchEvent方法對它進行處理
@Overridepublic boolean onTouchEvent(MotionEvent ev) {switch (ev.getActionMasked()) {case MotionEvent.ACTION_DOWN:mDownX = ev.getRawX();mDownY = ev.getRawY();//記錄按下的位置addIntoVelocity(ev);break;case MotionEvent.ACTION_MOVE:addIntoVelocity(ev);int deltaY = (int) (ev.getRawY() - mDownY);//計算手指移動距離,大于0表示手指往屏幕下方移動if (deltaY <= 0 && currentStatus!=STATUS_MOVING)return super.onTouchEvent(ev);if (deltaY>0||currentStatus==STATUS_MOVING){//如果往下移動,或者目前狀態(tài)是縮放移動狀態(tài),那么傳入移動坐標,進行對ImageView的操作setupMoving(ev.getRawX(),ev.getRawY());return super.onTouchEvent(ev);}break;case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:if (currentStatus!=STATUS_MOVING)return super.onTouchEvent(ev);float vY = computeYVelocity();if (vY>=1500||Math.abs(mUpY-mDownY)>screenHeight/4){//速度有一定快,或者豎直方向位移超過屏幕1/4,那么釋放//這里可以通過設置接口回調,外部Activity可以finish();}else {setupBack(mUpX,mUpY);}}return super.onTouchEvent(ev);}private void setupMoving(float movingX ,float movingY) {if (currentShowView == null)return;currentStatus = STATUS_MOVING;float deltaX = movingX - mDownX;float deltaY = movingY - mDownY;float scale = 1f;float alphaPercent = 1f;if(deltaY>0) {scale = 1 - Math.abs(deltaY) / screenHeight;alphaPercent = 1- Math.abs(deltaY) / (screenHeight/2);//這里是設置背景的透明度,我這是設置移動屏幕一半高度的距離就全透明了。}ViewHelper.setTranslationX(currentShowView, deltaX);ViewHelper.setTranslationY(currentShowView, deltaY);setupScale(scale);setupBackground(alphaPercent);}private void setupScale(float scale) {scale = Math.min(Math.max(scale, MIN_SCALE_WEIGHT), 1);//MIN_SCALE_WEIGHT是最小可縮小倍數,我這里設置的0.25fViewHelper.setScaleX(currentShowView, scale);ViewHelper.setScaleY(currentShowView, scale);}private void setupBackground(float percent){setBackgroundColor(convertPercentToBlackAlphaColor(percent));}//把0~1這透明度轉換成相應的黑色背景透明度,應該有更好的方式private int convertPercentToBlackAlphaColor(float percent){percent = Math.min(1, Math.max(0,percent));int intAlpha = (int) (percent*255);String stringAlpha = Integer.toHexString(intAlpha).toLowerCase();String color = "#"+(stringAlpha.length()<2?"0":"")+stringAlpha+"000000";return Color.parseColor(color);}private void setupBack(final float mUpX, final float mUpY){currentStatus = STATUS_BACK;if (mUpY!=mDownY) {ValueAnimator valueAnimator = ValueAnimator.ofFloat(mUpY, mDownY);valueAnimator.setDuration(BACK_DURATION);valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float mY = (float) animation.getAnimatedValue();float percent = (mY - mDownY) / (mUpY - mDownY);float mX = percent * (mUpX - mDownX) + mDownX;setupMoving(mX, mY);if (mY == mDownY) {mDownY = 0;mDownX = 0;currentStatus = STATUS_NORMAL;}}});valueAnimator.start();}else if (mUpX!=mDownX){ValueAnimator valueAnimator = ValueAnimator.ofFloat(mUpX, mDownX);valueAnimator.setDuration(BACK_DURATION);valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float mX = (float) animation.getAnimatedValue();float percent = (mX - mDownX) / (mUpX - mDownX);float mY = percent * (mUpY - mDownY) + mDownY;setupMoving(mX, mY);if (mX == mDownX) {mDownY = 0;mDownX = 0;currentStatus = STATUS_NORMAL;}}});valueAnimator.start();}else {//按下點的x,y值跟松開點的x,y值一樣,可以說明是點擊事件。}}private void addIntoVelocity(MotionEvent event){if (mVelocityTracker==null)mVelocityTracker = VelocityTracker.obtain();mVelocityTracker.addMovement(event);}private float computeYVelocity(){float result = 0;if (mVelocityTracker!=null){mVelocityTracker.computeCurrentVelocity(1000);result = mVelocityTracker.getYVelocity();releaseVelocity();}return result;}private void releaseVelocity(){if (mVelocityTracker!=null){mVelocityTracker.clear();mVelocityTracker.recycle();mVelocityTracker = null;}}
這里獲得觸摸位置的坐標一定要使用:
MotionEvent.getRawX()MotionEvent.getRawY()
如果使用:
MotionEvent.getX()MotionEvent.getY()
會出現坐標閃動很嚴重的問題,不明白的修改下自己運行看看就知道了。關于ViewHelper這個類大家應該都知道的是一個view的操作庫,引用方式為在build.gradle中加入:
compile 'com.nineoldandroids:library:2.4.0'setupBack()方法是對手勢放開之后,圖片歸位時的一些操作,簡單地利用valueAnimator,通過傳入按下點的mDownY值和放開點的mUpY值,模擬手勢移動,在動畫中不斷調用setupMoving(x,y)方法來產生圖片歸位的現象,這里有個mUpX!=mDownX和mUpY!=mDownY的判斷,是為了避免極限情況下比如只有豎直方向位移或者只有橫向位移導致動畫不正常。
使用ScaleViewPager的時候,在外部Activity或其他地方,每當ScaleViewPager切換了一張圖片,那就需要給viewpager再次設置要操作的view。
/*****你的圖片查看Activity.java*****/scaleViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {@Overridepublic void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {View imageView = 根據position獲得view;scaleViewPager.setCurrentShowView(imageView);}@Overridepublic void onPageSelected(int position) {newPageSelected = true;}@Overridepublic void onPageScrollStateChanged(int state) {}});/*****ScaleViewPager.java******/View currentShowView;public void setCurrentShowView(View currentShowView) {this.currentShowView = currentShowView;}
好了,到這里我們的關閉代碼就完成了,可以運行下看看效果,看上去能正常工作。但是我們的圖片瀏覽不可能這個viewpager只有一個item,當我們左右滑動的時候可以發(fā)現,我下拉的時候圖片被縮小,此時我還未松手,此時往左或往右移動,會發(fā)現在當前圖片縮放的同事,viewpager也跟著左右切換(drag)了,并且當我們在正常展示狀態(tài)下左右滑動(drag)想切換圖片時,當前的這個圖片不合時宜的響應了我們的手勢在進行縮放。這些現象是因為觸摸事件沖突了,要解決這個問題很簡單,首先,修改我們onTouchEvent中ACTION_MOVE事件的處理,當我們在move的時候,讓viewpager不要動,下面是修改后的代碼:
case MotionEvent.ACTION_MOVE:addIntoVelocity(ev);int deltaY = (int) (ev.getRawY() - mDownY);//這個地方從原來的<=0改成了DRAG_GAP_PX,目前DRAG_GAP_PX=50,意思是imageView響應縮放的手勢移動閾值,只有Y軸先移動了50px才能響應縮放,這個可以看情況改if (deltaY <= DRAG_GAP_PX && currentStatus!=STATUS_MOVING)return super.onTouchEvent(ev);//這個currentPageStatus在下面解釋if (currentPageStatus!=SCROLL_STATE_DRAGGING && (deltaY>DRAG_GAP_PX||currentStatus==STATUS_MOVING)){setupMoving(ev.getRawX(),ev.getRawY());return true;//把這里改為true就好了,true代表scaleviewpager已經消耗了touch事件,不調用super就不會調用到viewpager中的代碼,viewpager此時也就不會進行左右滑動了}break;
解決了縮放時viewpager不要滑動,我們還需要解決viewpager滑動時,當前view不要縮放,這就需要我們給viewpager額外設置滑動監(jiān)聽,修改ScaleViewPager.init()方法如下:
public void init(Context context){setBackgroundColor(Color.BLACK);addOnPageChangeListener(new OnPageChangeListener() {@Overridepublic void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}@Overridepublic void onPageSelected(int position) {}@Overridepublic void onPageScrollStateChanged(int state) {currentPageStatus = state;}});}
通過監(jiān)聽viewpager,記錄它的狀態(tài),結合上面修改過的ACTION_MOVE中的代碼:
currentPageStatus!=SCROLL_STATE_DRAGGING使得當viewpager滾動時,不調用setupMoving來解決沖突的問題。
下面放個完整代碼:
public class ScaleViewPager extends ViewPager {public static final int STATUS_NORMAL = 0;public static final int STATUS_MOVING = 1;public static final int STATUS_BACK = 2;public static final String TAG = "ScaleViewPager";//最多可縮小比例public static final float MIN_SCALE_WEIGHT = 0.25f;public static final int BACK_DURATION = 300;//mspublic static final int DRAG_GAP_PX = 50;private int currentStatus = STATUS_NORMAL;private int currentPageStatus;float mDownX;float mDownY;float screenHeight = ScreenUtil.getDisplayHeight();View currentShowView;private VelocityTracker mVelocityTracker;IPictureDrag iPictureDrag;public void setiPictureDrag(IPictureDrag iPictureDrag) {this.iPictureDrag = iPictureDrag;}public ScaleViewPager(Context context) {super(context);init(context);}public ScaleViewPager(Context context, AttributeSet attrs) {super(context, attrs);init(context);}public void init(Context context){setBackgroundColor(Color.BLACK);addOnPageChangeListener(new OnPageChangeListener() {@Overridepublic void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {Log.e(TAG,"in onPageScrolled positionOffset:"+positionOffset+" positionOffsetPixels:"+positionOffsetPixels);}@Overridepublic void onPageSelected(int position) {}@Overridepublic void onPageScrollStateChanged(int state) {currentPageStatus = state;}});}public void setCurrentShowView(View currentShowView) {this.currentShowView = currentShowView;}@Overridepublic boolean onTouchEvent(MotionEvent ev) {if (currentStatus == STATUS_BACK)return false;switch (ev.getActionMasked()) {case MotionEvent.ACTION_DOWN:mDownX = ev.getRawX();mDownY = ev.getRawY();addIntoVelocity(ev);break;case MotionEvent.ACTION_MOVE:addIntoVelocity(ev);int deltaY = (int) (ev.getRawY() - mDownY);if (deltaY <= DRAG_GAP_PX && currentStatus!=STATUS_MOVING)return super.onTouchEvent(ev);if (currentPageStatus!=SCROLL_STATE_DRAGGING && (deltaY>DRAG_GAP_PX||currentStatus==STATUS_MOVING)){setupMoving(ev.getRawX(),ev.getRawY());return true;}break;case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:if (currentStatus!=STATUS_MOVING)return super.onTouchEvent(ev);final float mUpX = ev.getRawX();//->mDownXfinal float mUpY = ev.getRawY();//->mDownYfloat vY = computeYVelocity();if (vY>=1500||Math.abs(mUpY-mDownY)>screenHeight/4){//速度有一定快,或者移動位置超過屏幕一半,那么釋放if (iPictureDrag!=null)iPictureDrag.onPictureRelease(currentShowView);}else {setupBack(mUpX,mUpY);}break;}return super.onTouchEvent(ev);}private void setupBack(final float mUpX, final float mUpY){currentStatus = STATUS_BACK;if (mUpY!=mDownY) {ValueAnimator valueAnimator = ValueAnimator.ofFloat(mUpY, mDownY);valueAnimator.setDuration(BACK_DURATION);valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float mY = (float) animation.getAnimatedValue();float percent = (mY - mDownY) / (mUpY - mDownY);float mX = percent * (mUpX - mDownX) + mDownX;setupMoving(mX, mY);if (mY == mDownY) {mDownY = 0;mDownX = 0;currentStatus = STATUS_NORMAL;}}});valueAnimator.start();}else if (mUpX!=mDownX){ValueAnimator valueAnimator = ValueAnimator.ofFloat(mUpX, mDownX);valueAnimator.setDuration(BACK_DURATION);valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float mX = (float) animation.getAnimatedValue();float percent = (mX - mDownX) / (mUpX - mDownX);float mY = percent * (mUpY - mDownY) + mDownY;setupMoving(mX, mY);if (mX == mDownX) {mDownY = 0;mDownX = 0;currentStatus = STATUS_NORMAL;}}});valueAnimator.start();}else if (iPictureDrag!=null)iPictureDrag.onPictureClick();}private void setupMoving(float movingX ,float movingY) {if (currentShowView == null)return;currentStatus = STATUS_MOVING;float deltaX = movingX - mDownX;float deltaY = movingY - mDownY;float scale = 1f;float alphaPercent = 1f;if(deltaY>0) {scale = 1 - Math.abs(deltaY) / screenHeight;alphaPercent = 1- Math.abs(deltaY) / (screenHeight/2);}ViewHelper.setTranslationX(currentShowView, deltaX);ViewHelper.setTranslationY(currentShowView, deltaY);setupScale(scale);setupBackground(alphaPercent);}private void setupScale(float scale) {scale = Math.min(Math.max(scale, MIN_SCALE_WEIGHT), 1);ViewHelper.setScaleX(currentShowView, scale);ViewHelper.setScaleY(currentShowView, scale);}private void setupBackground(float percent){setBackgroundColor(convertPercentToBlackAlphaColor(percent));}private int convertPercentToBlackAlphaColor(float percent){percent = Math.min(1, Math.max(0,percent));int intAlpha = (int) (percent*255);String stringAlpha = Integer.toHexString(intAlpha).toLowerCase();String color = "#"+(stringAlpha.length()<2?"0":"")+stringAlpha+"000000";return Color.parseColor(color);}private void addIntoVelocity(MotionEvent event){if (mVelocityTracker==null)mVelocityTracker = VelocityTracker.obtain();mVelocityTracker.addMovement(event);}private float computeYVelocity(){float result = 0;if (mVelocityTracker!=null){mVelocityTracker.computeCurrentVelocity(1000);result = mVelocityTracker.getYVelocity();releaseVelocity();}return result;}private void releaseVelocity(){if (mVelocityTracker!=null){mVelocityTracker.clear();mVelocityTracker.recycle();mVelocityTracker = null;}}}
源碼地址:
https://github.com/sbLaughing/DragCloseDemo1
到這里就結束啦。
