Android實現(xiàn)TV端大圖瀏覽效果的全過程
前言
最近TV開發(fā)需要加載的圖片很長,大小也很大,并且還不允許壓縮。比如顯示:世界地圖、清明上河圖、微博長圖、海報、活動照片等。
那么對于這種需求,該如何做呢?
首先不壓縮,按照原圖尺寸加載,那么屏幕肯定是不夠大的,并且考慮到內(nèi)存的情況,不可能一次性整圖加載到內(nèi)存中,所以肯定是局部加載,那么就需要用到一個類:
BitmapRegionDecoder
其次,既然屏幕顯示不完,那么最起碼要添加一個上下左右拖動的手勢,讓用戶可以拖動查看。
實現(xiàn)方式有很多:
1.BitmapRegionDecoder:
分片加載,使用系統(tǒng)BitmapRegionDecoder去加載本地的圖片,調(diào)用bitmapRegionDecoder.decodeRegion解析圖片的矩形區(qū)域,返回bitmap,最終顯示在ImageView上。這種方案需要手動處理滑動、縮放手勢,網(wǎng)絡(luò)圖片還要處理緩存策略等問題。實現(xiàn)方式比較繁瑣也不是很推薦。
2.SubsamplingScaleImageView
一款封裝 BitmapRegionDecoder的三方庫,已經(jīng)處理了滑動,縮放手勢。我們可以考慮選擇這個庫來進行加載長圖,但是官方上的Demo示例加載的長圖均為本地圖片。這可能并不符合我們的網(wǎng)絡(luò)場景需求,所以對于網(wǎng)絡(luò)圖片,我們還要考慮不同的加載框架,
3.Glide+SubsamplingScaleImageView混合加載渲染
對于圖片加載框架,Glide當(dāng)然是首選,我們使用Glide進行網(wǎng)絡(luò)圖片的下載和緩存管理,F(xiàn)ileTarget作為橋梁,SubsamplingScaleImageView進行本地資源圖片的分片加載,看起來很靠譜,那么一起來實現(xiàn)吧。
4.自定義LongImageView,結(jié)合BitmapRegionDecoder使用
本文采取的第四種方式,代碼如下:
public class LongImageView extends AppCompatImageView {
private String TAG = getClass().getSimpleName();
private int mTargetY = 0;
private int scrollDistance = 0;
private int imgWidth = 0, imgHeight = 0;
private Bitmap imgBitmap = null;
private Bitmap holderBitmap;
private BitmapRegionDecoder bitmapRegionDecoder;
private boolean isScrolling = false;
private float startY = -1;
private int mStartTargetY = -1;
private BitmapFactory.Options scaleOptions = new BitmapFactory.Options();
public Bitmap bitmap;
public static final String EVENT_PROP_URL = "url";
public static final String EVENT_PROP_BITMAP_WIDTH = "resourceWidth";
public static final String EVENT_PROP_BITMAP_HEIGHT = "resourceHeight";
?
public LongImageView(Context context) {
super(context);
init();
}
?
public LongImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
?
public LongImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
?
private void init() {
//遙控器按鍵事件
setOnKeyListener(new OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
scrollDistance = scrollDistance <= 0 ? getHeight() : scrollDistance;
if (event.getAction() == KeyEvent.ACTION_DOWN && !isScrolling) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_UP:
scrollBy(0 - viewHeight2ImageHeight(scrollDistance));
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
scrollBy(viewHeight2ImageHeight(scrollDistance));
break;
}
}
return false;
}
?
});
//響應(yīng)鼠標(biāo)拖拽(手指也可以)
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, " touch down");
startY = event.getRawY();
mStartTargetY = mTargetY;
break;
case MotionEvent.ACTION_MOVE:
float currentY = event.getRawY();
mTargetY = mStartTargetY + (int) (viewHeight2ImageHeight((int) (startY - currentY)) * 1f);
mTargetY = Math.max(0, Math.min(mTargetY, imgHeight - viewHeight2ImageHeight(getHeight())));
Log.e(TAG, " touch move " + mTargetY);
invalidate();
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, " touch up");
startY = -1;
break;
}
return true;
}
});
}
?
private void startScroll(int targetY) {
targetY = Math.max(0, Math.min(targetY, imgHeight - viewHeight2ImageHeight(getHeight())));
ValueAnimator valueAnimator = ValueAnimator.ofInt(mTargetY, targetY);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mTargetY = (int) animation.getAnimatedValue();
invalidate();
}
});
valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
isScrolling = true;
}
?
@Override
?
public void onAnimationEnd(Animator animation) {
isScrolling = false;
}
?
@Override
?
public void onAnimationCancel(Animator animation) {
isScrolling = false;
}
?
@Override
?
public void onAnimationRepeat(Animator animation) {
}
});
valueAnimator.setInterpolator(new LinearInterpolator());
//設(shè)置滑動速度
valueAnimator.setDuration(10);
valueAnimator.start();
}
?
?
/**
* 根據(jù)InputStream 生成 BitmapRegionDecoder
*
* @param imgStream
*/
public void setImageStream(InputStream imgStream) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(imgStream, new Rect(0, 0, 0, 0), options);
imgWidth = options.outWidth;
imgHeight = options.outHeight;
//尋找最佳的縮放比例
int viewHeight2ImageHeight = viewHeight2ImageHeight(getHeight());
//設(shè)置縮放比例
int scale = getScaleValue(imgWidth, viewHeight2ImageHeight, 1);
scaleOptions.inSampleSize = scale;
} catch (Exception e) {
e.printStackTrace();
}
try {
bitmapRegionDecoder = BitmapRegionDecoder.newInstance(imgStream, false);
} catch (Exception e) {
e.printStackTrace();
}
}
?
/**
* 根據(jù)圖片文件 生成 BitmapRegionDecoder
*
* @param imgFile
*/
public void setImageFile(File imgFile) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(imgFile.getAbsolutePath(), options);
imgWidth = options.outWidth;
imgHeight = options.outHeight;
//尋找最佳的縮放比例
int viewHeight2ImageHeight = viewHeight2ImageHeight(getHeight());
int scale = getScaleValue(imgWidth, viewHeight2ImageHeight, 1);
scaleOptions.inSampleSize = scale;
} catch (Exception e) {
e.printStackTrace();
}
try {
bitmapRegionDecoder = BitmapRegionDecoder.newInstance(imgFile.getAbsolutePath(), false);
} catch (Exception e) {
e.printStackTrace();
}
}
?
private int getScaleValue(int imgWidth, int imgHeight, int scaleValue) {
long memory = Runtime.getRuntime().maxMemory() / 4;
if (memory > 0) {
if (imgWidth * imgHeight * 4 > memory) {
scaleValue += 1;
return getScaleValue(imgWidth, imgHeight, scaleValue);
}
}
return scaleValue;
}
?
/**
* 根據(jù)圖片Id 生成 BitmapRegionDecoder
*
* @param resourceId
*/
@SuppressLint("ResourceType")
public void setImageResource(int resourceId) {
InputStream imgStream = getResources().openRawResource(resourceId);
setImageStream(imgStream);
}
?
/**
* 設(shè)置占位圖
*
* @param holderId
*/
public void setPlaceHolder(int holderId) {
holderBitmap = BitmapFactory.decodeResource(getResources(), holderId);
}
?
/**
* 滑動到具體的位置
*
* @param targetY
*/
private void scrollTo(int targetY) {
startScroll(targetY);
}
?
/**
* 設(shè)置相對于當(dāng)前,繼續(xù)滑動的距離。小于0 向上滑動,大于0向下滑動
*
* @param distance
*/
private void scrollBy(int distance) {
startScroll(mTargetY + distance);
}
?
/**
* 設(shè)置每次滑動的距離
*
* @param scrollDistance
*/
public void setScrollDistance(int scrollDistance) {
this.scrollDistance = scrollDistance;
}
?
@Override
protected void onDraw(Canvas canvas) {
Log.e(getClass().getSimpleName(), "draw start " + getWidth() + " " + getHeight());
canvas.save();
int sr = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
Paint paint = new Paint();
paint.setAntiAlias(true);
if (bitmapRegionDecoder != null) {
int targetHeight = viewHeight2ImageHeight(getHeight());//根據(jù)控件的高度獲取需要在原始圖片上截取的高度
Log.e(getClass().getSimpleName(), "targetHeight " + targetHeight);
Log.e(getClass().getSimpleName(), "draw resource "
+ " " + imgWidth + " " + imgHeight
+ " " + mTargetY + " " + targetHeight);
imgBitmap = null;
if (imgHeight - mTargetY >= targetHeight) {//剩余區(qū)域大于 當(dāng)前控件高度
imgBitmap = bitmapRegionDecoder.decodeRegion(new Rect(0, mTargetY, imgWidth, mTargetY + targetHeight)
, scaleOptions);
} else {//剩余區(qū)域小于 當(dāng)前控件高度
imgBitmap = bitmapRegionDecoder.decodeRegion(new Rect(0, imgHeight - targetHeight, imgWidth, imgHeight)
, scaleOptions);
}
if (imgBitmap != null) {
//繪制需要展示的圖片
canvas.drawBitmap(imgBitmap
, new Rect(0, 0, imgBitmap.getWidth(), imgBitmap.getHeight())
, new Rect(0, 0, getWidth(), getHeight())
, paint);
}
imgBitmap = null;
holderBitmap = null;
} else {
if (holderBitmap != null) {//繪制占位圖
canvas.drawBitmap(holderBitmap
, new Rect(0, 0, holderBitmap.getWidth(), holderBitmap.getHeight())
, new Rect(0, 0, getWidth(), getHeight())
, paint);
}
}
canvas.restoreToCount(sr);
canvas.restore();
Log.e(getClass().getSimpleName(), "draw end");
}
?
/**
* 圖片高度轉(zhuǎn)為相對于控件的高度
*
* @param imgHeight
* @return
*/
private int imageHeight2ViewHeight(int imgHeight) {
if (this.imgHeight <= 0) {
return 0;
}
return (int) (imgHeight / ((float) getWidth() / imgWidth * imgHeight) * getHeight());
}
?
/**
* 控件高度轉(zhuǎn)為相對于圖片高度
*
* @param viewHeight
* @return
*/
private int viewHeight2ImageHeight(int viewHeight) {
if (getHeight() <= 0) {
return 0;
}
return (int) (viewHeight / ((float) getWidth() / imgWidth * imgHeight) * imgHeight);
}
?
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
//回收資源和釋放內(nèi)存
release();
}
?
public void release() {
if (imgBitmap != null && !imgBitmap.isRecycled()) {
imgBitmap.recycle();
imgBitmap = null;
}
if (holderBitmap != null && !holderBitmap.isRecycled()) {
holderBitmap.recycle();
holderBitmap = null;
}
System.gc();
}
}5.在MainActivity中的使用:
/**
* @auth: njb
* @date: 2022/11/7 0:11
* @desc:
*/
public class MainActivity extends AppCompatActivity {
public String url = "https://qcloudimg-moss.cp47.ott.cibntv.net/data_center/files/2022/10/26/67a66d35-3f7c-4de8-9dfe-c706e42f44f2.jpg";
?
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
?
private void initView() {
LongImageView mImageView = findViewById(R.id.imageView);
mImageView.setScrollDistance((int) ((float) ScreenUtils.getScreenHeight(this) / 3 * 2));
mImageView.setFocusable(true);
try {
Glide.with(this)
.load(url)
.downloadOnly(new SimpleTarget<File>() {
@Override
public void onResourceReady(@NonNull File resource, @Nullable Transition<? super File> transition) {
mImageView.setImageFile(resource);
?
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}6.布局文件代碼:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.example.longimageView.view.LongImageView
android:id="@+id/imageView"
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"/>
</androidx.constraintlayout.widget.ConstraintLayout>7.實現(xiàn)的效果如下:

總結(jié)
到此這篇關(guān)于Android實現(xiàn)TV端大圖瀏覽效果的文章就介紹到這了,更多相關(guān)Android TV端大圖瀏覽內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android開發(fā)基礎(chǔ)簡化Toast調(diào)用方法詳解
這篇文章主要為大家介紹了Android開發(fā)基礎(chǔ)簡化Toast調(diào)用方法的相關(guān)資料,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02
Android自定義View實現(xiàn)BMI指數(shù)條
這篇文章主要為大家詳細(xì)介紹了Android自定義View實現(xiàn)BMI指數(shù)條,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-06-06
使用Android studio3.6的java api方式調(diào)用opencv
這篇文章主要介紹了Android studio3.6的java api方式調(diào)用opencv的代碼詳解,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-03-03
Android中在WebView里實現(xiàn)Javascript調(diào)用Java類的方法
這篇文章主要介紹了Android中在WebView里實現(xiàn)Javascript調(diào)用Java類的方法,本文直接給出示例,需要的朋友可以參考下2015-03-03

