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

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

浏览效果

photoview001

photoview002

由于简书有图片大小限制,压缩后很模糊,
请移步github查看。

功能实现

  • 超大图浏览

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

  • 浏览Activity背景透明

        
        //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> 
         
    
  • 沉浸式效果

        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事件

         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格式,而且在不同的设备和版本上表现有差异,一句话:不稳定,还出问题。

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

源码地址

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

代码位置:uikit module中photoviewer包下

App图片处理库推荐

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

本文为原创内容,转载请说明出处,首发博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容