Android高雅地完结TV端超长大图阅读

Android优雅地实现TV端超长大图浏览

1.前言

之前在Android和Vue端都完结过长图加载,虽然完结需求,但是有许多问题没有处理,作用也不尽人意今日就各种问题来剖析一下:

  • 图片加载时清晰度不是很好,会失真的情况?
  • 图片超越屏幕限制咋处理?
  • 假如是超长巨图加载怎样办?20M的图片?试过714*13987的图片吗?
  • 图片缓存怎样做?怎样保证每次加载速度?缓存的整理咋处理?
  • 怎样保证图片加载大图不溃散且速度很快?
  • 图片最多加载20000*20000?会溃散吗?超越多少会溃散?

为了处理以上问题自己找到了各种计划进行尝试,包含之前的比如,终究决议选用SubsamplingScaleImageView这个库,

首先此库有7.6k的star,运用进程中发现图片缓存和内存处理得非常好,当然此库是针对手机端开发的,TV端的大图库很少,搞理解原理其实不区分手机仍是TV、盒子、投影等,我是在此库的根底上封装了一套适用于TV端的超长大图加载东西类,一起封装了一套适用于vue的图片加载库,不过是在腾讯的hippy根底之上封装的,本文先讲解TV端高雅地加载超长大图计划,下一篇讲解vue的封装和计划。

2.完结功用

  • 显现巨大的图画或长图,大图能够加载到内存中
  • 在扩大时显现高分辨率细节
  • 现在测试过最多加载20,000×20,000像素的图片,但较大的图画加载速度较慢

3.简略简介:

  • SubsamplingScaleImageView是一个适用于 Android 的自定义图画视图,专为相片库规划并显现没有 OutOfMemoryErrors 的大图画(例如地图和建筑计划)。包括手指缩放、平移、旋转和动画支撑,并答应轻松扩展,以便您能够增加自己的覆盖和接触工作检测。
  • 该视图可选择运用子采样和图块来支撑非常大的图画 – 加载低分辨率根底层,当您扩大时,它会覆盖可见区域的较小高分辨率图块。这避免了在内存中保存过多的数据。它非常合适显现大图画,一起答应您扩大到高分辨率细节。您能够为较小的图画和显现位图目标禁用平铺。禁用平铺有一些优点和缺点,因而要决议哪个最好,请参阅 wiki

4.完结流程:

Android优雅地实现TV端超长大图浏览

5.源码地址:

github.com/davemorriss…

6.首要办法:

办法 描绘 参数
onReady() 图片资源已预备
onImageLoaded() 图片已加载
onPreviewLoadError() 图片预览失利 Exception(预览失利反常信息)
onImageLoadError() 图片加载失利 Exception(加载失利反常信息)
onTileLoadError() 图片无法加载时调用 Exception(加载失利反常信息)
onPreviewReleased() 图片预览完结后收回位图
onScaleChanged(); 图片缩放份额产生改动 float newScale, int origin
onCenterChanged(); 图片的中心点产生改动 PointF newCenter, int origin
doubleTapZoom(); 是否能够双指缩放 PointF sCenter, PointF vFocus
setMinimumDpi(); 答应设置的屏幕最小密度 int dpi
setMaximumDpi(); 答应设置的屏幕最大密度 int dpi
setDoubleTapZoomDpi(); 设置双指缩放的密度 int dpi
setGestureDetector(); 设置手势监听工作 Context context
setOrientation(); 设置图片方向 int orientation

7.首要工作:

7.1 图片加载的工作接口监听:

public interface OnImageEventListener

  • void onReady();
  • void onImageLoaded();
  • void onPreviewLoadError(Exception e);
  • void onImageLoadError(Exception e);
  • void onTileLoadError(Exception e);
  • void onPreviewReleased();
public interface OnImageEventListener {
​
  void onReady();
​
  void onImageLoaded();
  
  void onPreviewLoadError(Exception e);
​
  void onImageLoadError(Exception e);
​
  void onTileLoadError(Exception e);
​
  void onPreviewReleased();
}

7.2.图片状况产生改动工作监听:

public interface OnStateChangedListener

  • void onScaleChanged(float newScale, int origin);
  • void onCenterChanged(PointF newCenter, int origin);
public interface OnStateChangedListener {
    /**
     * 图片缩放份额产生改动
     * @params newScale 新的缩放份额
     * @params origin  工作的来历
     */
  void onScaleChanged(float newScale, int origin);
  /**
     * 图片的中心点产生改动
     * @params newScale 新的中心点
     * @params origin  工作的来历
     */
  void onCenterChanged(PointF newCenter, int origin);
​
}

7.3 图片加载动画工作监听

OnAnimationEventListene

  • void onComplete();
  • void onInterruptedByUser();
  • void onInterruptedByNewAnim();
public interface OnAnimationEventListener{
    /**
     * 动画现已完结
     */
    void onComplete();
​
    /**
     * 由于用户接触了屏幕,动画在到达结尾之前已间断。
     */
    void onInterruptedByUser();
​
    /**
     * 由于新的动画已开端,动画在到达结尾之前已间断。
     */
    void onInterruptedByNewAnim();
}

8.首要特点:

参数 描绘 类型
PAN_LIMIT_INSIDE 显现在图片内部 int
PAN_LIMIT_OUTSIDE 显现超出图片范围 int
PAN_LIMIT_CENTER 显现在屏幕中心 int
SCALE_TYPE_CENTER_INSIDE 缩放处于图片内部款式 int
SCALE_TYPE_CENTER_CROP 缩放裁剪款式 int
SCALE_TYPE_CUSTOM 自定义缩放款式 int
SCALE_TYPE_START 图片开端缩放方位款式 int
ORIENTATION_USE_EXIF 默许旋转视点 int
ORIENTATION_0 旋转视点0 int
ORIENTATION_90 旋转视点90 int
ORIENTATION_180 旋转视点180 int
ORIENTATION_270 旋转视点270 int
bitmapIsPreview 是否运用预览位图 boolean
bitmapIsCached 是否运用缓存 boolean
orientation 视点 int
maxScale 答应缩放的最大份额 float
minScale 答应缩放的最小份额 float
minimumTileDpi 答应的最小密度 int
panLimit 图片缩放款式 int

9.运用说明:

此库默许是支撑本地图片的,假如能够拿到图片的资源id,assert或者文件路径,直接运用下面办法进行运用:

SubsamplingScaleImageView imageView = (SubsamplingScaleImageView)findViewById(id.imageView);
imageView.setImage(ImageSource.resource(R.drawable.monkey));
// ... or ...
imageView.setImage(ImageSource.asset("big1.png"))
// ... or ...
imageView.setImage(ImageSource.uri("/sdcard/DCIM/DSCM00123.JPG"));
复制代码
假如能够拿到 bitmap 就能够这么运用:
​
SubsamplingScaleImageView imageView = (SubsamplingScaleImageView)findViewById(id.imageView);
imageView.setImage(ImageSource.bitmap(bitmap));
能够看到运用是非常简略的。
​
本文在此根底之上增加了网络图片的运用,代码如下:
//运用网络图片之前需求增加网络恳求权限,这个做Android开发的根本上都知道
<uses-permission android:name="android.permission.INTERNET"/>
//适配Androidhttps恳求权限
android:usesCleartextTraffic="true"
​
public String url = "https://www.6hu.cc/wp-content/uploads/2023/07/1688189660-90e48084618ffc7.jpeg#pic_center";
​
private void initView() {
    easyTVLongView = findViewById(R.id.easyLongView);
    easyTVLongView.setFocusable(true);
    easyTVLongView.requestFocus();
    easyTVLongView.setLongImages(url);
}

10.源码剖析:

先看ImageSource:

在前面的运用进程中,发现SubsamplingScaleImageView都是依据 ImageSource 来进行操控的,

// 减缩之后的部分源码
public final class ImageSource {
​
  static final String FILE_SCHEME = "file:///";
  static final String ASSET_SCHEME = "file:///android_asset/";
​
  private final Uri uri;
  private final Bitmap bitmap;
  private final Integer resource;
  private boolean tile;
  private int sWidth;
  private int sHeight;
  private Rect sRegion;
  private boolean cached;
​
  private ImageSource(int resource) {
    this.bitmap = null;
    this.uri = null;
    this.resource = resource;
    this.tile = true;
   }
 }
  • 简略来说,ImageSource 的作用跟它的命名是相同的,用来处理图片地址来历,最终 SubsamplingScaleImageView 也是从它获取图片的。这个类有好几个特点, uri bitmap resource这几个便是图片的来历, 还有几个是图片的尺寸,而咱们调用的结构办法里边首要是resource和tile这两个特点, tile = true说明支撑部分加载特点。
  • 这个也是咱们需求借鉴的。当咱们再写一个图片库的时分,除了支撑网络图片,也要考虑其他场景,比如对本地图片和资源的支撑。还有便是假如你不知道怎样去支撑的时分,这时分就能够看看 ImageSource 的完结。这便是咱们为啥需求读源码,学习源码。
  • 这儿还有个点需求注意的是,假如直接给 bitmap 传给 ImageSource 是不会触发瓦片式加载的。由于整个图片的 bitmap 现已存在了,在做瓦片式含义不大。

再看setImage 办法

public final void setImage(@NonNull ImageSource imageSource, ImageSource previewSource, ImageViewState state) {
    //noinspection ConstantConditions
    if (imageSource == null) {
      throw new NullPointerException("imageSource must not be null");
     }
​
    reset(true);
    if (state != null) { restoreState(state); }
​
    if (previewSource != null) {
      if (imageSource.getBitmap() != null) {
        throw new IllegalArgumentException("Preview image cannot be used when a bitmap is provided for the main image");
       }
      if (imageSource.getSWidth() <= 0 || imageSource.getSHeight() <= 0) {
        throw new IllegalArgumentException("Preview image cannot be used unless dimensions are provided for the main image");
       }
      this.sWidth = imageSource.getSWidth();
      this.sHeight = imageSource.getSHeight();
      this.pRegion = previewSource.getSRegion();
      if (previewSource.getBitmap() != null) {
        this.bitmapIsCached = previewSource.isCached();
        onPreviewLoaded(previewSource.getBitmap());
       } else {
        Uri uri = previewSource.getUri();
        if (uri == null && previewSource.getResource() != null) {
          uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + getContext().getPackageName() + "/" + previewSource.getResource());
         }
        BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, true);
        execute(task);
       }
     }
​
    if (imageSource.getBitmap() != null && imageSource.getSRegion() != null) {
      onImageLoaded(Bitmap.createBitmap(imageSource.getBitmap(), imageSource.getSRegion().left, imageSource.getSRegion().top, imageSource.getSRegion().width(), imageSource.getSRegion().height()), ORIENTATION_0, false);
     } else if (imageSource.getBitmap() != null) {
      onImageLoaded(imageSource.getBitmap(), ORIENTATION_0, imageSource.isCached());
     } else {
      sRegion = imageSource.getSRegion();
      uri = imageSource.getUri();
      if (uri == null && imageSource.getResource() != null) {
        uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + getContext().getPackageName() + "/" + imageSource.getResource());
       }
      if (imageSource.getTile() || sRegion != null) {
        // Load the bitmap using tile decoding.
        TilesInitTask task = new TilesInitTask(this, getContext(), regionDecoderFactory, uri);
        execute(task);
       } else {
        // Load the bitmap as a single image.
        BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false);
        execute(task);
       }
     }
   }

然后看TilesInitTask和BitmapLoadTask

private static class TilesInitTask extends AsyncTask<Void, Void, int[]> {
    @Override
    protected int[] doInBackground(Void... params) {
      try {
        String sourceUri = source.toString();
        Context context = contextRef.get();
        DecoderFactory<? extends ImageRegionDecoder> decoderFactory = decoderFactoryRef.get();
        SubsamplingScaleImageView view = viewRef.get();
        if (context != null && decoderFactory != null && view != null) {
          view.debug("TilesInitTask.doInBackground");            // 获取decoder 
          decoder = decoderFactory.make();
          Point dimensions = decoder.init(context, source);
          int sWidth = dimensions.x;
          int sHeight = dimensions.y;
          int exifOrientation = view.getExifOrientation(context, sourceUri);            // 获取 region,或者说修正 region
          if (view.sRegion != null) {
            view.sRegion.left = Math.max(0, view.sRegion.left);
            view.sRegion.top = Math.max(0, view.sRegion.top);
            view.sRegion.right = Math.min(sWidth, view.sRegion.right);
            view.sRegion.bottom = Math.min(sHeight, view.sRegion.bottom);
            sWidth = view.sRegion.width();
            sHeight = view.sRegion.height();
           }
          return new int[] { sWidth, sHeight, exifOrientation };
         }
       } catch (Exception e) {
        Log.e(TAG, "Failed to initialise bitmap decoder", e);
        this.exception = e;
       }
      return null;
     }
​
    @Override
    protected void onPostExecute(int[] xyo) {
      final SubsamplingScaleImageView view = viewRef.get();
      if (view != null) {
        if (decoder != null && xyo != null && xyo.length == 3) {
          view.onTilesInited(decoder, xyo[0], xyo[1], xyo[2]);
         } else if (exception != null && view.onImageEventListener != null) {
          view.onImageEventListener.onImageLoadError(exception);
         }
       }
     }
   }

在后台履行的首要工作是调用了解码器decoder的初始化办法,获取图片的宽高信息,然后再回到主线程调用onTilesInited办法通知现已初始化完结。咱们先看初始化办法做的工作,先找到解码器,内置的解码器工厂如下,

private DecoderFactory<? extends ImageRegionDecoder> regionDecoderFactory = new CompatDecoderFactory<ImageRegionDecoder>(SkiaImageRegionDecoder.class);

所以咱们只需看看 SkiaImageRegionDecoder 这个decoder 既可:

public class SkiaImageRegionDecoder implements ImageRegionDecoder {

private BitmapRegionDecoder decoder;
private final ReadWriteLock decoderLock = new ReentrantReadWriteLock(true);
private static final String FILE_PREFIX = "file://";
private static final String ASSET_PREFIX = FILE_PREFIX + "/android_asset/";
private static final String RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + "://";
private final Bitmap.Config bitmapConfig;
@Keep
@SuppressWarnings("unused")
public SkiaImageRegionDecoder() {
    this(null);
}
@SuppressWarnings({"WeakerAccess", "SameParameterValue"})
public SkiaImageRegionDecoder(@Nullable Bitmap.Config bitmapConfig) {
    Bitmap.Config globalBitmapConfig = SubsamplingScaleImageView.getPreferredBitmapConfig();
    if (bitmapConfig != null) {
        this.bitmapConfig = bitmapConfig;
    } else if (globalBitmapConfig != null) {
        this.bitmapConfig = globalBitmapConfig;
    } else {       // 假如没有传配置,就会运用 565 的办法,这样一个像素占有2个字节,16位 = 5+6+5
        this.bitmapConfig = Bitmap.Config.RGB_565;
    }
}
@Override
@NonNull      // 总结起来便是依据不同的图片资源类型来选择合适的 regiondecoder 进行解析,终究回来的是图片的宽高。
public Point init(Context context, @NonNull Uri uri) throws Exception {
    String uriString = uri.toString();
    if (uriString.startsWith(RESOURCE_PREFIX)) {
        Resources res;
        String packageName = uri.getAuthority();
        if (context.getPackageName().equals(packageName)) {
            res = context.getResources();
        } else {
            PackageManager pm = context.getPackageManager();
            res = pm.getResourcesForApplication(packageName);
        }
        int id = 0;
        List<String> segments = uri.getPathSegments();
        int size = segments.size();
        if (size == 2 && segments.get(0).equals("drawable")) {
            String resName = segments.get(1);
            id = res.getIdentifier(resName, "drawable", packageName);
        } else if (size == 1 && TextUtils.isDigitsOnly(segments.get(0))) {
            try {
                id = Integer.parseInt(segments.get(0));
            } catch (NumberFormatException ignored) {
            }
        }
        decoder = BitmapRegionDecoder.newInstance(context.getResources().openRawResource(id), false);
    } else if (uriString.startsWith(ASSET_PREFIX)) {
        String assetName = uriString.substring(ASSET_PREFIX.length());
        decoder = BitmapRegionDecoder.newInstance(context.getAssets().open(assetName, AssetManager.ACCESS_RANDOM), false);
    } else if (uriString.startsWith(FILE_PREFIX)) {
        decoder = BitmapRegionDecoder.newInstance(uriString.substring(FILE_PREFIX.length()), false);
    } else {
        InputStream inputStream = null;
        try {
            ContentResolver contentResolver = context.getContentResolver();
            inputStream = contentResolver.openInputStream(uri);
            if (inputStream == null) {
                throw new Exception("Content resolver returned null stream. Unable to initialise with uri.");
            }
            decoder = BitmapRegionDecoder.newInstance(inputStream, false);
        } finally {
            if (inputStream != null) {
                try { inputStream.close(); } catch (Exception e) { /* Ignore */ }
            }
        }
    }
    return new Point(decoder.getWidth(), decoder.getHeight());
}

SkiaImageRegionDecoder 首要便是依据图片资源类型选择一个合适的 RegionDecoder。接下去再看 onTilesInited

// overrides for the dimensions of the generated tiles 省略无关的代码
    public static final int TILE_SIZE_AUTO = Integer.MAX_VALUE;
    private int maxTileWidth = TILE_SIZE_AUTO;
    private int maxTileHeight = TILE_SIZE_AUTO;
        this.decoder = decoder;
        this.sWidth = sWidth;
        this.sHeight = sHeight;
        this.sOrientation = sOrientation;
        checkReady();
        if (!checkImageLoaded() && maxTileWidth > 0 && maxTileWidth != TILE_SIZE_AUTO && maxTileHeight > 0 && maxTileHeight != TILE_SIZE_AUTO && getWidth() > 0 && getHeight() > 0) {
            initialiseBaseLayer(new Point(maxTileWidth, maxTileHeight));
        }
        invalidate();
        requestLayout();

这儿就将相关参数都传给 SubsamplingScaleImageView 了,后续就能够直接用了。能够看到最终调用了invalidate 和 requestLayout,也就说终究会触发重绘操作。

onMeasure

比较简略,这块就直接略过了。

ondraw

下面直接看 ondraw 办法。ondraw 的办法很长,咱们首要看一些要害逻辑:

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        createPaints();
        // When using tiles, on first render with no tile map ready, initialise it and kick off async base image loading.
        if (tileMap == null && decoder != null) {
            initialiseBaseLayer(getMaxBitmapDimensions(canvas));
        }
        preDraw();
        if (tileMap != null && isBaseLayerReady()) {
            // Optimum sample size for current scale
            int sampleSize = Math.min(fullImageSampleSize, calculateInSampleSize(scale));
            // First check for missing tiles - if there are any we need the base layer underneath to avoid gaps
            boolean hasMissingTiles = false;
            for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) {
                if (tileMapEntry.getKey() == sampleSize) {
                    for (Tile tile : tileMapEntry.getValue()) {
                        if (tile.visible && (tile.loading || tile.bitmap == null)) {
                            hasMissingTiles = true;
                        }
                    }
                }
            }
            // Render all loaded tiles. LinkedHashMap used for bottom up rendering - lower res tiles underneath.
            for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) {
                if (tileMapEntry.getKey() == sampleSize || hasMissingTiles) {
                    for (Tile tile : tileMapEntry.getValue()) {
                        sourceToViewRect(tile.sRect, tile.vRect);
                        if (!tile.loading && tile.bitmap != null) {
                            if (tileBgPaint != null) {
                                canvas.drawRect(tile.vRect, tileBgPaint);
                            }
                            if (matrix == null) { matrix = new Matrix(); }
                            matrix.reset();
                            setMatrixArray(srcArray, 0, 0, tile.bitmap.getWidth(), 0, tile.bitmap.getWidth(), tile.bitmap.getHeight(), 0, tile.bitmap.getHeight());
                            matrix.setPolyToPoly(srcArray, 0, dstArray, 0, 4);
                            canvas.drawBitmap(tile.bitmap, matrix, bitmapPaint);
                        } 
                }
            }
        } else if (bitmap != null) {
            float xScale = scale, yScale = scale;
            if (bitmapIsPreview) {
                xScale = scale * ((float)sWidth/bitmap.getWidth());
                yScale = scale * ((float)sHeight/bitmap.getHeight());
            }
            if (matrix == null) { matrix = new Matrix(); }
            matrix.reset();
            matrix.postScale(xScale, yScale);
            matrix.postRotate(getRequiredRotation());
            matrix.postTranslate(vTranslate.x, vTranslate.y);
            if (tileBgPaint != null) {
                if (sRect == null) { sRect = new RectF(); }
                sRect.set(0f, 0f, bitmapIsPreview ? bitmap.getWidth() : sWidth, bitmapIsPreview ? bitmap.getHeight() : sHeight);
                matrix.mapRect(sRect);
                canvas.drawRect(sRect, tileBgPaint);
            }
            canvas.drawBitmap(bitmap, matrix, bitmapPaint);
        }
   }

onDraw首要做了几件事,initialiseBaseLayer,设置tileMap,最终便是先优先tileMap进行drawBitmap,再取bitmap制作,咱们先看看initialiseBaseLayer做了什么。

initialiseBaseLayer

private synchronized void initialiseBaseLayer(@NonNull Point maxTileDimensions) {
        debug("initialiseBaseLayer maxTileDimensions=%dx%d", maxTileDimensions.x, maxTileDimensions.y);
        satTemp = new ScaleAndTranslate(0f, new PointF(0, 0));  // 先给定一个初始值
        fitToBounds(true, satTemp);  // 居中
        // Load double resolution - next level will be split into four tiles and at the center all four are required,
        // so don't bother with tiling until the next level 16 tiles are needed.
        fullImageSampleSize = calculateInSampleSize(satTemp.scale);  // 核算采样率,要不要samplesize
        if (fullImageSampleSize > 1) {
            fullImageSampleSize /= 2;
        }
        if (fullImageSampleSize == 1 && sRegion == null && sWidth() < maxTileDimensions.x && sHeight() < maxTileDimensions.y) {
            // Whole image is required at native resolution, and is smaller than the canvas max bitmap size.
            // Use BitmapDecoder for better image support. 不需求regiondecoder ,直接加载图片
            decoder.recycle();
            decoder = null;
            BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false);
            execute(task);
        } else {
       // 需求进行瓦片化加载
            initialiseTileMap(maxTileDimensions);
       // 首先取出当时屏幕需求的采样率, fullImageSampleSIze 便是当时屏幕所需求的采样率,并不是对map所有的数据都进行解压
            List<Tile> baseGrid = tileMap.get(fullImageSampleSize);
            for (Tile baseTile : baseGrid) {
                TileLoadTask task = new TileLoadTask(this, decoder, baseTile);
                execute(task);
            }       // 依照要求来加载展现图片,一起对不是该采样率的 bitmap 进行收回
            refreshRequiredTiles(true);
        }
    }

ScaleAndTranslate是存储了制作的时分的偏移量和缩放等级,调用 fitToBounds 其实便是先对根本的偏移方位等设置好。然后核算选用率来决议要不要进行 regiondecoder。

下面直接看 regiondecoder 相关逻辑。首先是要对 TileMap 进行初始化。

private void initialiseTileMap(Point maxTileDimensions) {
        debug("initialiseTileMap maxTileDimensions=%dx%d", maxTileDimensions.x, maxTileDimensions.y);
        this.tileMap = new LinkedHashMap<>();
        int sampleSize = fullImageSampleSize;  // 采样率
        int xTiles = 1;
        int yTiles = 1;
        while (true) { // 死循环
            int sTileWidth = sWidth()/xTiles;  // 即将被采样的图片巨细
            int sTileHeight = sHeight()/yTiles;
            int subTileWidth = sTileWidth/sampleSize; // 采样率下的图片巨细
            int subTileHeight = sTileHeight/sampleSize;       // maxTileDimensions 本质上便是 cavas 能够支撑的最大宽高,这儿调整 subtileWidth 的宽度,使得其能够显现在屏幕上,这儿需求注意的是,一块tile 其实还包含1/4的不行见区域(屏幕外)
            while (subTileWidth + xTiles + 1 > maxTileDimensions.x || (subTileWidth > getWidth() * 1.25 && sampleSize < fullImageSampleSize)) {
                xTiles += 1;
                sTileWidth = sWidth()/xTiles;
                subTileWidth = sTileWidth/sampleSize;          // 当采样率为1的时分,由于此刻采样后图片仍旧远远大于屏幕宽度,因而,会被切割成块数也会更多
            }
            while (subTileHeight + yTiles + 1 > maxTileDimensions.y || (subTileHeight > getHeight() * 1.25 && sampleSize < fullImageSampleSize)) {
                yTiles += 1;
                sTileHeight = sHeight()/yTiles;
                subTileHeight = sTileHeight/sampleSize;
            }       // 终究划分的块数
            List<Tile> tileGrid = new ArrayList<>(xTiles * yTiles);
            for (int x = 0; x < xTiles; x++) {
                for (int y = 0; y < yTiles; y++) {
                    Tile tile = new Tile();
                    tile.sampleSize = sampleSize;
                    tile.visible = sampleSize == fullImageSampleSize;  // 当时是否可见
                    tile.sRect = new Rect(
                        x * sTileWidth,
                        y * sTileHeight,
                        x == xTiles - 1 ? sWidth() : (x + 1) * sTileWidth,
                        y == yTiles - 1 ? sHeight() : (y + 1) * sTileHeight
                    );
                    tile.vRect = new Rect(0, 0, 0, 0);
                    tile.fileSRect = new Rect(tile.sRect);
                    tileGrid.add(tile);
                }
            }       // 以采样率当做key 值,对应的 list 分块当做value
            tileMap.put(sampleSize, tileGrid);       // 采样率为1 就退出
            if (sampleSize == 1) {
                break;
            } else {
                sampleSize /= 2;
            }
        }
    }

这儿望文生义便是切片,在不同的采样率的情况下切成一个个的tile,由于是进行部分加载,所以在扩大的时分,要取出对应的采样率的图片,继而取出对应的区域,试想一下,假如扩大几倍,仍然用的16的采样率,那么图片扩大之后肯定很含糊,所以缩放等级不同,要运用不同的采样率解码图片。这儿的tileMap是一个Map,key是采样率,value是一个列表,列表存储的是对应key采样率的所有切片集合,如下图:

Android优雅地实现TV端超长大图浏览

fileSRect是一个切片的矩阵巨细,每一个切片的矩阵巨细要保证在对应的缩放等级和采样率下能够显现正常。 初始化切片之后,就履行当时采样率下的TileLoadTask。

/**
     * Async task used to load images without blocking the UI thread.
     */
    private static class TileLoadTask extends AsyncTask<Void, Void, Bitmap> {
        private final WeakReference<SubsamplingScaleImageView> viewRef;
        private final WeakReference<ImageRegionDecoder> decoderRef;
        private final WeakReference<Tile> tileRef;
        private Exception exception;
        TileLoadTask(SubsamplingScaleImageView view, ImageRegionDecoder decoder, Tile tile) {
            this.viewRef = new WeakReference<>(view);
            this.decoderRef = new WeakReference<>(decoder);
            this.tileRef = new WeakReference<>(tile);
            tile.loading = true;
        }
        @Override
        protected Bitmap doInBackground(Void... params) {
            try {
                SubsamplingScaleImageView view = viewRef.get();
                ImageRegionDecoder decoder = decoderRef.get();
                Tile tile = tileRef.get();
                if (decoder != null && tile != null && view != null && decoder.isReady() && tile.visible) {
                    view.debug("TileLoadTask.doInBackground, tile.sRect=%s, tile.sampleSize=%d", tile.sRect, tile.sampleSize);
                    view.decoderLock.readLock().lock();
                    try {
                        if (decoder.isReady()) {
                            // Update tile's file sRect according to rotation 假如用户有过操作,需求对 rect 进行调整
                            view.fileSRect(tile.sRect, tile.fileSRect);
                            if (view.sRegion != null) {
                                tile.fileSRect.offset(view.sRegion.left, view.sRegion.top);
                            }
                            return decoder.decodeRegion(tile.fileSRect, tile.sampleSize);
                        } else {
                            tile.loading = false;
                        }
                    } finally {
                        view.decoderLock.readLock().unlock();
                    }
                } else if (tile != null) {
                    tile.loading = false;
                }
            } catch (Exception e) {
                Log.e(TAG, "Failed to decode tile", e);
                this.exception = e;
            } catch (OutOfMemoryError e) {
                Log.e(TAG, "Failed to decode tile - OutOfMemoryError", e);
                this.exception = new RuntimeException(e);
            }
            return null;
        }
        @Override
        protected void onPostExecute(Bitmap bitmap) {
            final SubsamplingScaleImageView subsamplingScaleImageView = viewRef.get();
            final Tile tile = tileRef.get();
            if (subsamplingScaleImageView != null && tile != null) {
                if (bitmap != null) {
                    tile.bitmap = bitmap;
                    tile.loading = false;
                    subsamplingScaleImageView.onTileLoaded();
                } else if (exception != null && subsamplingScaleImageView.onImageEventListener != null) {
                    subsamplingScaleImageView.onImageEventListener.onTileLoadError(exception);
                }
            }
        }
    }
    /**
     * Called by worker task when a tile has loaded. Redraws the view.
     */
    private synchronized void onTileLoaded() {
        debug("onTileLoaded");
        checkReady();
        checkImageLoaded();
        if (isBaseLayerReady() && bitmap != null) {
            if (!bitmapIsCached) {
                bitmap.recycle();
            }
            bitmap = null;
            if (onImageEventListener != null && bitmapIsCached) {
                onImageEventListener.onPreviewReleased();
            }
            bitmapIsPreview = false;
            bitmapIsCached = false;
        }
        invalidate();  // 进行重绘
    }

整体而言,没太多复杂逻辑,这儿选用异步加载来获取bitmap,中心会调整 filerect,bitmap 解压完结后,就会从头制作。

preDraw

没有太多逻辑,首要便是制作前一些预备工作,包括缩放,方位等等。

isBaseLayerReady

首要便是看 tileMap 里边的 bitmap 是否预备好了。

for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) {
                if (tileMapEntry.getKey() == sampleSize || hasMissingTiles) {
                    for (Tile tile : tileMapEntry.getValue()) {
                        sourceToViewRect(tile.sRect, tile.vRect);
                        if (!tile.loading && tile.bitmap != null) {
                            if (tileBgPaint != null) {
                                canvas.drawRect(tile.vRect, tileBgPaint);
                            }
                            matrix.reset();
                            setMatrixArray(srcArray, 0, 0, tile.bitmap.getWidth(), 0, tile.bitmap.getWidth(), tile.bitmap.getHeight(), 0, tile.bitmap.getHeight());
                            setMatrixArray(dstArray, tile.vRect.left, tile.vRect.top, tile.vRect.right, tile.vRect.top, tile.vRect.right, tile.vRect.bottom, tile.vRect.left, tile.vRect.bottom);
                            matrix.setPolyToPoly(srcArray, 0, dstArray, 0, 4);
                            canvas.drawBitmap(tile.bitmap, matrix, bitmapPaint);
                        }
                    }
                }
  }

这便是切片制作的要害代码,在Tile这个类中,sRect担任保存切片的原始巨细,vRect则担任保存切片的制作巨细,所以 sourceToViewRect(tile.sRect, tile.vRect) 这儿进行了矩阵的缩放,其实便是依据之前核算得到的scale对图片原始巨细进行缩放。 接着再经过矩阵改换,将图片巨细改换为制作巨细进行制作。剖析到这儿,其实整个的加载进程和逻辑现已是了解得七七八八了。 还有另外的便是手势缩放的处理,经过监听move等接触工作,然后从头核算scale的巨细,接着经过scale的巨细去从头得到对应的采样率,持续经过tileMap取出采样率下对应的切片,对切片恳求解码。值得一提的是,在move工作的时分,这儿做了优化,解码的图片并没有进行制作,而是对原先采样率下的图片进行缩放,直到监听到up工作,才会去从头制作对应采样率下的图片。所以在缩放的进程中,会看到一个含糊的图画,其实便是高采样率下的图片进行扩大导致的。比及缩放结束,会从头制作,图片就显现正常了。

12.简略封装:

这儿有2种加载办法:

  • 经过遥控器上下左右移动随机加载图片区域
  • 经过遥控器上下滑动加载超长大图

完结办法1

package com.example.longimageView.view;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.widget.Scroller;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.blankj.utilcode.util.LogUtils;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.transition.Transition;
import com.davemorrissey.labs.subscaleview.ImageSource;
import com.davemorrissey.labs.subscaleview.ImageViewState;
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
import com.example.longimageView.BuildConfig;
import com.example.longimageView.R;
import com.example.longimageView.utils.ScreenUtils;
import java.util.Random;
/**
 * @author: njb
 * @date: 2023/4/10 20:22
 * @desc:TV端超长巨图加载组件
 */
public class EasyTVLongView extends SubsamplingScaleImageView implements SubsamplingScaleImageView.OnImageEventListener {
    private static final String TAG = "EasyLongViewLog";
    private Scroller mScroller;
    private final int scrollHeight = 200;
    private int scrollY = 0;
    private float defaultScale = 1.0f;
    private boolean isFocus = false;
    private Paint paint = new Paint();
    private PointF vPoint = new PointF();
    private PointF sPoint;
    private Bitmap bitmap;
    public EasyTVLongView(Context context, AttributeSet attr) {
        super(context, attr);
        initBitmap();
        initialise(context);
    }
    public EasyTVLongView(Context context) {
        super(context);
        initBitmap();
        initialise(context);
    }
    private void initialise(Context context) {
        mScroller = new Scroller(context);
        this.setOnFocusChangeListener((v, hasFocus) -> {
            isFocus = hasFocus;
        });
        this.setOnKeyListener((v, keyCode, event) -> {
            setDispatchKeyEvent(event);
            return false;
        });
    }
    public void setDispatchKeyEvent(KeyEvent event) {
        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT || event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT || event.getKeyCode() == KeyEvent.KEYCODE_DPAD_DOWN || event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP || event.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER) {
                if(isFocus){
                    play();
                }
            }
        }
    }
    /**
     * 加载图片数据
     * @param url
     */
    public void setLongImages(String url) {
        try {
            Glide.with(this.getContext())
                    .asBitmap()
                    .load(url)
                    .diskCacheStrategy(DiskCacheStrategy.ALL)
                    .into(new SimpleTarget<Bitmap>() {
                        @Override
                        public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
                            EasyTVLongView.this.setImage(ImageSource.cachedBitmap(resource), new ImageViewState(defaultScale, new PointF(0, 0), 0));
                            if (BuildConfig.DEBUG) {
                                LogUtils.d(TAG, "---图片宽度为---- " + resource.getWidth() + "---图片高度为---" + resource.getHeight());
                            }
                            EasyTVLongView.this.onImageLoaded();
                        }
                        @Override
                        public void onDestroy() {
                            super.onDestroy();
                            EasyTVLongView.this.recycle();
                        }
                    });
            EasyTVLongView.this.setFocusable(true);
        } catch (Exception e) {
            e.printStackTrace();
            EasyTVLongView.this.onImageLoadError(e);
        }
    }
    public void play() {
        Random random = new Random();
        if (this.isReady()) {
            float maxScale = this.getMaxScale();
            float minScale = this.getMinScale();
            float scale = (random.nextFloat() * (maxScale - minScale)) + minScale;
            PointF center = new PointF(random.nextInt(this.getSWidth()), random.nextInt(this.getSHeight()));
            this.setPin(center);
            SubsamplingScaleImageView.AnimationBuilder animationBuilder = this.animateScaleAndCenter(scale, center);
            if (this.getSHeight() == 0) {
                if (animationBuilder != null) {
                    animationBuilder.withDuration(2000).withEasing(EASE_OUT_QUAD).withInterruptible(false).start();
                }
            } else {
                if (animationBuilder != null) {
                    animationBuilder.withDuration(500).start();
                }
            }
        }
    }
    public void setPin(PointF sPin) {
        this.sPoint = sPin;
        initBitmap();
        invalidate();
    }
    private void initBitmap() {
        float density = getResources().getDisplayMetrics().densityDpi;
        bitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.pushpin_blue);
        float w = (density / 420f) * bitmap.getWidth();
        float h = (density / 420f) * bitmap.getHeight();
        bitmap = Bitmap.createScaledBitmap(bitmap, (int) w, (int) h, true);
    }
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (!isReady()) {
            return;
        }
        paint.setAntiAlias(true);
        if (sPoint != null && bitmap != null) {
            sourceToViewCoord(sPoint, vPoint);
            float vX = vPoint.x - (bitmap.getWidth() / 2);
            float vY = vPoint.y - bitmap.getHeight();
            canvas.drawBitmap(bitmap, vX, vY, paint);
        }
    }
    @Override
    public void onReady() {
        if (BuildConfig.DEBUG) {
            LogUtils.d(TAG, "图片资源已预备" + this.isReady());
        }
    }
    @Override
    public void onImageLoaded() {
        if (BuildConfig.DEBUG) {
            LogUtils.d(TAG, "图片已加载" + this.isImageLoaded());
        }
    }
    @Override
    public void onPreviewLoadError(Exception e) {
        if (BuildConfig.DEBUG) {
            LogUtils.e(TAG, "图片预览失利" + e.getMessage());
        }
    }
    @Override
    public void onImageLoadError(Exception e) {
        if (BuildConfig.DEBUG) {
            LogUtils.e(TAG, "图片加载失利" + e.getMessage());
        }
    }
    @Override
    public void onTileLoadError(Exception e) {
        if (BuildConfig.DEBUG) {
            LogUtils.e(TAG, "图片平铺加载失利" + e.getMessage());
        }
    }
    @Override
    public void onPreviewReleased() {
        if (BuildConfig.DEBUG) {
            if (this.getDrawingCache() != null) {
                LogUtils.d(TAG, "图片预加载资源已收回" + this.getDrawingCache().isRecycled());
            }
        }
    }
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        this.recycle();
    }
}

13.完结作用:

Android优雅地实现TV端超长大图浏览

14.完结办法2:

经过上下滑动加载超长大图

package com.example.longimageView.view;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.widget.Scroller;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.blankj.utilcode.util.LogUtils;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.transition.Transition;
import com.davemorrissey.labs.subscaleview.ImageSource;
import com.davemorrissey.labs.subscaleview.ImageViewState;
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
import com.example.longimageView.BuildConfig;
import com.example.longimageView.R;
import com.example.longimageView.utils.ScreenUtils;
import java.util.Random;
/**
 * @author: njb
 * @date: 2023/4/10 20:22
 * @desc:TV端超长巨图加载组件
 */
public class EasyTVLongView extends SubsamplingScaleImageView implements SubsamplingScaleImageView.OnImageEventListener {
    private static final String TAG = "EasyLongViewLog";
    private Scroller mScroller;
    private final int scrollHeight = 200;
    private int scrollY = 0;
    private float defaultScale = 1.0f;
    private boolean isFocus = false;
    private Paint paint = new Paint();
    private PointF vPoint = new PointF();
    private PointF sPoint;
    private Bitmap bitmap;
    public EasyTVLongView(Context context, AttributeSet attr) {
        super(context, attr);
      //  initBitmap();
        initialise(context);
    }
    public EasyTVLongView(Context context) {
        super(context);
        //initBitmap();
        initialise(context);
    }
    private void initialise(Context context) {
        mScroller = new Scroller(context);
        this.setOnFocusChangeListener((v, hasFocus) -> {
            isFocus = hasFocus;
        });
        this.setOnKeyListener((v, keyCode, event) -> {
            setDispatchKeyEvent(event);
            return false;
        });
    }
    public void setDispatchKeyEvent(KeyEvent event) {
       if (event.getAction() == KeyEvent.ACTION_DOWN) {
            if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP) {
                if (isFocus) {
                    startSmoothScrollUp(0, 50);
                }
            }
            if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_DOWN) {
                if (isFocus) {
                    startSmoothScrollDown(0, 50);
                }
            }
        }
    }
    /**
     * 向下滑动
     * @param desX
     * @param ms
     */
    public void startSmoothScrollDown(int desX, int ms) {
        int startX = getScrollX();
        int startY = getScrollY();
        scrollY = getScrollY();
        int scrollDownHeight = this.getSHeight() - scrollY - scrollHeight;
        //startScroll(x起始坐标,y起始坐标,x方向偏移值,y方向偏移值,翻滚时长)
        if ((scrollDownHeight + scrollHeight) > ScreenUtils.getScreenHeight(this.getContext())) {
            mScroller.startScroll(startX, startY, desX - startX, scrollHeight, ms);
        }
        if (BuildConfig.DEBUG) {
            LogUtils.d(TAG, "----向下滑动间隔---" + scrollDownHeight + "----滑动y坐标----" + startY);
        }
        invalidate();
    }
    /**
     * 向上滑动
     *
     * @param desX
     * @param ms
     */
    public void startSmoothScrollUp(int desX, int ms) {
        int startX = getScrollX();
        int startY = getScrollY();
        scrollY = getScrollY();
        if (BuildConfig.DEBUG) {
            LogUtils.d(TAG, "向上滑动滑动x坐标" + startX + "----滑动y坐标----" + startY);
        }
        if (scrollY > ScreenUtils.dip2px(this.getContext(), 60)) {
            mScroller.startScroll(0, startY, desX, -scrollHeight, ms);
        }
        invalidate();
    }
    /**
     * 加载图片数据
     *
     * @param url
     */
    public void setLongImages(String url) {
        try {
            Glide.with(this.getContext())
                    .asBitmap()
                    .load(url)
                    .diskCacheStrategy(DiskCacheStrategy.ALL)
                    .into(new SimpleTarget<Bitmap>() {
                        @Override
                        public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
                            EasyTVLongView.this.setImage(ImageSource.cachedBitmap(resource), new ImageViewState(defaultScale, new PointF(0, 0), 0));
                            if (BuildConfig.DEBUG) {
                                LogUtils.d(TAG, "---图片宽度为---- " + resource.getWidth() + "---图片高度为---" + resource.getHeight());
                            }
                            EasyTVLongView.this.onImageLoaded();
                        }
                        @Override
                        public void onDestroy() {
                            super.onDestroy();
                            EasyTVLongView.this.recycle();
                        }
                    });
            EasyTVLongView.this.setFocusable(true);
        } catch (Exception e) {
            e.printStackTrace();
            EasyTVLongView.this.onImageLoadError(e);
        }
    }
    public void play() {
        Random random = new Random();
        if (this.isReady()) {
            float maxScale = this.getMaxScale();
            float minScale = this.getMinScale();
            float scale = (random.nextFloat() * (maxScale - minScale)) + minScale;
            PointF center = new PointF(random.nextInt(this.getSWidth()), random.nextInt(this.getSHeight()));
            this.setPin(center);
            SubsamplingScaleImageView.AnimationBuilder animationBuilder = this.animateScaleAndCenter(scale, center);
            if (this.getSHeight() == 0) {
                if (animationBuilder != null) {
                    animationBuilder.withDuration(2000).withEasing(EASE_OUT_QUAD).withInterruptible(false).start();
                }
            } else {
                if (animationBuilder != null) {
                    animationBuilder.withDuration(500).start();
                }
            }
        }
    }
    public void setPin(PointF sPin) {
        this.sPoint = sPin;
        initBitmap();
        invalidate();
    }
    private void initBitmap() {
        float density = getResources().getDisplayMetrics().densityDpi;
        bitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.pushpin_blue);
        float w = (density / 420f) * bitmap.getWidth();
        float h = (density / 420f) * bitmap.getHeight();
        bitmap = Bitmap.createScaledBitmap(bitmap, (int) w, (int) h, true);
    }
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (!isReady()) {
            return;
        }
        paint.setAntiAlias(true);
        if (sPoint != null && bitmap != null) {
            sourceToViewCoord(sPoint, vPoint);
            float vX = vPoint.x - (bitmap.getWidth() / 2);
            float vY = vPoint.y - bitmap.getHeight();
            canvas.drawBitmap(bitmap, vX, vY, paint);
        }
    }
    @Override
    public void onReady() {
        if (BuildConfig.DEBUG) {
            LogUtils.d(TAG, "图片资源已预备" + this.isReady());
        }
    }
    @Override
    public void onImageLoaded() {
        if (BuildConfig.DEBUG) {
            LogUtils.d(TAG, "图片已加载" + this.isImageLoaded());
        }
    }
    @Override
    public void onPreviewLoadError(Exception e) {
        if (BuildConfig.DEBUG) {
            LogUtils.e(TAG, "图片预览失利" + e.getMessage());
        }
    }
    @Override
    public void onImageLoadError(Exception e) {
        if (BuildConfig.DEBUG) {
            LogUtils.e(TAG, "图片加载失利" + e.getMessage());
        }
    }
    @Override
    public void onTileLoadError(Exception e) {
        if (BuildConfig.DEBUG) {
            LogUtils.e(TAG, "图片平铺加载失利" + e.getMessage());
        }
    }
    @Override
    public void onPreviewReleased() {
        if (BuildConfig.DEBUG) {
            if (this.getDrawingCache() != null) {
                LogUtils.d(TAG, "图片预加载资源已收回" + this.getDrawingCache().isRecycled());
            }
        }
    }
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        this.recycle();
    }
}

15.完结作用:

Android优雅地实现TV端超长大图浏览

16.简略运用:

/**
 * @auth: njb
 * @date: 2022/11/7 0:11
 * @desc:
 */
public class MainActivity extends AppCompatActivity {
    // public String url = "https://www.6hu.cc/wp-content/uploads/2023/01/1672911223-d89ea1e734eb783.jpg";
    public String url = "https://www.6hu.cc/wp-content/uploads/2023/07/1688189660-90e48084618ffc7.jpeg#pic_center";
    private EasyTVLongView easyTVLongView;
    private static final String TAG = "MainLog";
    private StringBuilder stringBuilder = new StringBuilder();
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }
    private void initView() {
        easyTVLongView = findViewById(R.id.easyLongView);
        easyTVLongView.setFocusable(true);
        easyTVLongView.requestFocus();
        easyTVLongView.setLongImages(url);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}

17.布局代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <com.example.longimageView.view.EasyTVLongView
        android:id="@+id/easyLongView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:focusable="true"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:visibility="visible"/>
    <TextView
        android:id="@+id/tv_up"
        android:layout_width="180dp"
        android:layout_height="60dp"
        android:background="@color/purple_700"
        android:textColor="@color/white"
        android:text="向下滑动"
        android:gravity="center"
        android:textSize="20sp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@id/tv_down"
        app:layout_constraintTop_toTopOf="parent"
        android:visibility="gone"/>
    <TextView
        android:id="@+id/tv_down"
        android:layout_width="180dp"
        android:layout_height="60dp"
        android:background="@color/purple_700"
        android:text="向上滑动"
        android:textColor="@color/white"
        android:gravity="center"
        android:textSize="20sp"
        android:visibility="gone"
        android:layout_marginStart="20dp"
        app:layout_constraintLeft_toRightOf="@id/tv_up"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <com.example.longimageView.view.LongImageView
        android:id="@+id/scrollCustomView"
        android:layout_width="match_parent"
        android:layout_height="400dp"
        android:visibility="gone"
        android:background="@color/colorPrimary"
        tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>

18.注意事项:

  • 默许现已适配屏幕和图片巨细,只需设置图片url即可,不需处理焦点、图片适配等
  • 现在现已在Android底层做了图片收回和紧缩处理,支撑手势缩放、平移动画、上下滑动等
  • 支撑jpg、png、webp等多种格式的预览,屏幕旋转后轻松恢复份额、中心和方向
  • 缓存图片运用了两种策略:一个是glide的磁盘缓存,一个是自带的图片缓存ImageSource.cachedBitmap

19.运用总结:

以上便是今日的内容,从源码剖析到库的流程图,在到封装和详细运用,不仅完结了本地图片加载,还完结了网络图片加载,在加载进程中还有一个从含糊到高清的渐变作用,这个是比较友爱的体会,并且测试了10多种机型的TV和盒子,几百台不同系统和类型的TV,根本上没有出现问题,最多加载20,000×20,000的图片,不过图片越大第一次加载就比较耗时,假如超越20,000×20,000根本上就不要考虑,由于很少有加载这么大的图片,机器本身的性能也扛不住,本文是经过多轮模拟器和实在设备的测试,现已用于实际项目中,当然不是TV开发就不会有这个需求,手机最多便是加一个扩大预览或者加载webP格式的长图,不过已然有这个需求,就要学会剖析和找出处理办法,期间遇到了许多问题,缓存处理、内存收回和图片清晰度、oom等都是令人头疼的问题,特别是封装成适用于vue的长图加载库,遇到的问题更多,下篇将讲解vue的长图库封装和实践。原创不易,且看且珍惜!!好了,打卡收工,关机睡觉~~

20.项目源码:

gitee.com/jackning_ad…