Android类似微信图片查看下拉缩放并移动

龙旋

共 17100字,需浏览 35分钟

 · 2022-08-24

平时做开发的时候图片查看肯定是经常碰到的一个功能,在做社交App或内容浏览App的时候,图片查看的关闭方式做的人性化一点,肯定会给用户留下正面的印象。这里是仿照微信的图片关闭方式撸的代码,提供一点思路。


首先上一张效果图:



再上一张微信效果图对比:



观察上面两张图,我们总结出这个图片手势关闭的特点:


  1. 手指下拉图片,图片会被移动并且缩小,同时背景也有个透明度的变化;


  2. 正常状态按下,往上移动,是不能对图片进行移动的,但是往下拉一下之后就可以上下左右随便移动;


要实现上面的效果,我们需要操作图片查看Activity中的ViewPager的事件获取,所以这里自定义一个ScaleViewPager继承系统的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方法对它进行处理

    @Override    public 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){                    //如果往下移动,或者目前状态是缩放移动状态,那么传入移动坐标,进行对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.25f ViewHelper.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() { @Override public 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() { @Override public 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() {            @Override            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {               View imageView = 根据position获得view;               scaleViewPager.setCurrentShowView(imageView);            }
@Override public void onPageSelected(int position) { newPageSelected = true; }
@Override public void onPageScrollStateChanged(int state) {
} });
/*****ScaleViewPager.java******/ View currentShowView; public void setCurrentShowView(View currentShowView) { this.currentShowView = currentShowView; }


好了,到这里我们的关闭代码就完成了,可以运行下看看效果,看上去能正常工作。但是我们的图片浏览不可能这个viewpager只有一个item,当我们左右滑动的时候可以发现,我下拉的时候图片被缩小,此时我还未松手,此时往左或往右移动,会发现在当前图片缩放的同事,viewpager也跟着左右切换(drag)了,并且当我们在正常展示状态下左右滑动(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额外设置滑动监听,修改ScaleViewPager.init()方法如下:

 public void init(Context context){        setBackgroundColor(Color.BLACK);        addOnPageChangeListener(new OnPageChangeListener() {            @Override            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {            }
@Override public void onPageSelected(int position) { }
@Override public void onPageScrollStateChanged(int state) { currentPageStatus = state; } });    }


通过监听viewpager,记录它的状态,结合上面修改过的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;//ms public 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() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { Log.e(TAG,"in onPageScrolled positionOffset:"+positionOffset+" positionOffsetPixels:"+positionOffsetPixels); }
@Override public void onPageSelected(int position) {
}
@Override public void onPageScrollStateChanged(int state) { currentPageStatus = state; } }); }

public void setCurrentShowView(View currentShowView) { this.currentShowView = currentShowView; }
@Override public 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();//->mDownX final float mUpY = ev.getRawY();//->mDownY
float 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() { @Override public 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() { @Override public 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


到这里就结束啦。


浏览 37
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报