安卓图片浏览(支持超大图,附源码)

大图浏览可以说是所有App必备功能,可见其重要性,所以有必要将其独立,便于维护和复用。本文代码基于SubsamplingScaleImageView开源库实现,增加单手拖拽返回,透明度变化等效果。

浏览效果

功能实现

  • 超大图浏览

    SubsamplingScaleImageView基于BitmapRegionDecoder实现,避免OOM情况下,轻松浏览超大图,支持各种手势操作。感兴趣的请查阅其源码。

  • 浏览Activity背景透明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //manifest 设置Activity theme属性
    android:theme="@style/photoviewer_theme"
    //对应style定义
    <style name="photoviewer_theme" parent="Theme.AppCompat.NoActionBar">
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowIsTranslucent">true</item>
    </style>
  • 沉浸式效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    private void hideSystemUI() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    View decorView = getWindow().getDecorView();
    decorView.setSystemUiVisibility(
    View.SYSTEM_UI_FLAG_LAYOUT_STABLE
    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
    | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
    | View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar
    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
    } else {
    WindowManager.LayoutParams attrs = getWindow().getAttributes();
    attrs.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
    getWindow().setAttributes(attrs);
    getWindow().addFlags(
    WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
    }
    //change navigationbar bg color
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
    getWindow().setNavigationBarColor(Color.TRANSPARENT);
    }
    }
  • 自定义DragPhotoView拦截touch事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
    //如果正在动画,拦截不处理。
    if (isAnimating()) {
    return true;
    }
    //判断图片是否为初始状态(第一次进入后显示的大小)
    if (isReady() && (firstDisplayScale == getScale())) {
    final int action = event.getAction();
    switch (action & MotionEvent.ACTION_MASK) {
    case MotionEvent.ACTION_DOWN:
    if (DEBUG) {
    Log.d(TAG, "action_down = " + firstDisplayScale + ", getScale = " + getScale() + " isFirstEnterState = " + (firstDisplayScale == getScale()));
    }
    downX = event.getX();
    downY = event.getY();
    canDrag = true;
    break;
    case MotionEvent.ACTION_MOVE:
    if (canDrag) {
    final float dy = Math.abs(downY - event.getY());
    if (firstDisplayScale == getScale() && dy > touchSlop) {
    isDragging = true;
    translateX = event.getX() - downX;
    translateY = event.getY() - downY;
    float percent = dy / maxTranslateY;
    if (percent > 1.0f) {
    percent = 1.0f;
    }
    bgAlpha = (int) (255 * (1 - percent));
    bgAlpha = bgAlpha < minBgAlpha ? minBgAlpha : bgAlpha;
    //尽量将图片缩放比例设置小点
    final float p = dy / getHeight();
    bitmapScale = (1 - p);
    bitmapScale = bitmapScale < minBitmapScale ? minBitmapScale : bitmapScale;
    if(DEBUG) {
    Log.d(TAG, "action_move translateX = " + translateX + "; translateY = " + translateY + "; pointerCount = " + event.getPointerCount());
    }
    invalidate();
    }
    }
    break;
    case MotionEvent.ACTION_CANCEL:
    case MotionEvent.ACTION_UP:
    if(DEBUG) {
    Log.d(TAG, "action = " + action + "; pointerCount = " + event.getPointerCount());
    }
    if (isDragging) {
    //最后一个手指离开屏幕时,检查y轴方向拖拽距离是否超过阀值,若超过则回调dismiss接口
    if (Math.abs(translateY) >= maxTranslateY && dismissListener != null) {
    dismissListener.onDismiss();
    }else{
    //若为超过阀值,则指定动画回到初始位置。
    restoreFirstEnterState();
    }
    isDragging = false;
    }
    break;
    case MotionEvent.ACTION_POINTER_UP:
    case MotionEvent.ACTION_POINTER_DOWN:
    if(DEBUG){
    Log.d(TAG, "action pointer down or up= " + action + "; pointerCount = " + event.getPointerCount());
    }
    //防止拖拽过程中多点触摸导致事件错乱。
    canDrag = isDragging;
    break;
    default:
    break;
    }
    }
    return isDragging;
    }
    });
    @Override
    protected void onDraw(Canvas canvas) {
    //肯定touch move事件,不断更新以下几个变量的值来达到动画效果。
    createPaint();
    bgPaint.setAlpha(bgAlpha);
    canvas.drawRect(0, 0, getWidth(), getHeight(), bgPaint);
    canvas.translate(translateX, translateY);
    canvas.scale(bitmapScale, bitmapScale, getWidth() / 2, getHeight() / 2);
    super.onDraw(canvas);
    }
  • 点击位置(进入和退出动画)

    // TODO 非刚需,暂未实现。

兼容处理(18.03.03更新)

  • Image failed to decode using JPEG decoder

    由于使用了SubsamplingScaleImageView库显示大图,内部使用的是系统BitmapRegionDecoder解码,其只支持JPG和PNG格式,而且在不同的设备和版本上表现有差异,一句话:不稳定,容易出问题。官方给出的方案Custom-decoders

    我的解决思路:监听SubsamplingScaleImageView解码错误回调,转为使用PhotoView显示图片。

源码地址

以上提及代码均在AndroidUiKit项目(安卓常用UI组件库。 总结、沉淀、封装优化;为避免重复造轮子,此项目会收集优秀的三方库,或直接引用,或修改源码;目标很明确:快速集成开发,提高效率。)

代码位置:uikit module中photoviewer包下

App图片处理库推荐

  • 图片加载神器Glide
  • Multi-media selector(本地图片视频选择器) AndroidUiKit项目中有推荐。
0%