在移动应用开发中,媒体文件的预览功能是不可或缺的一部分。无论是图片、视频还是音频文件,都需要一个高效且易用的解决方案来满足用户需求。本文将介绍两个常用的开源库:com.github.chrisbanes:PhotoView
和 ExoPlayer
,并探讨它们在媒体文件预览场景中的优劣及待优化的地方。
一、com.github.chrisbanes:PhotoView
简介
com.github.chrisbanes:PhotoView
是一个用于 Android 的开源库,提供了增强的 ImageView
功能,支持手势缩放、平移等交互操作。它非常适合用于图片预览场景,例如相册应用、图片详情页等。
主要功能
- 手势缩放和平移
支持双指缩放和单指拖动图片的功能。 - 多种缩放类型
提供了不同的缩放模式(如FIT_CENTER
,CENTER_CROP
等),可以根据需求设置图片的显示方式。 - 简单易用
只需将PhotoView
替换掉普通的ImageView
,即可实现丰富的交互功能。 - 兼容性好
兼容大多数 Android 版本,适合老旧设备和现代设备使用。
优势
- 轻量级:代码简洁,依赖少,易于集成。
- 性能优越:手势操作流畅,内存占用低。
- 扩展性强:可以通过自定义实现更多功能,如加载 GIF 图片或网络图片。
待优化地方
- 手势控制
默认的手势识别逻辑可能无法完全满足复杂场景需求(如多点触控冲突)。开发者需要根据具体需求进行调整。 - 动画效果
缺乏内置的过渡动画支持,可能需要额外引入动画库来提升用户体验。 - 图片加载优化
对于大图或高分辨率图片,可能会出现内存溢出问题,建议结合图片加载库(如 Glide 或 Picasso)使用。
二、ExoPlayer
简介
ExoPlayer
是一款基于 Android 中的低层级媒体 API 构建的应用级媒体播放器。与 Android 内置的 MediaPlayer
相比,ExoPlayer
具有多项优势。它支持 MediaPlayer
支持的许多媒体格式,还支持 DASH 和 SmoothStreaming 等自适应格式。ExoPlayer
具有高度的可定制性和可扩展性,因此能够用于许多高级用例。它是 Google 应用(包括 YouTube 和 Google Play 影视)使用的开源项目。
官方文档
主要功能
- 支持多种媒体格式
包括常见的 MP4、MKV、FLV 等格式,以及自适应流媒体格式(DASH、SmoothStreaming)。 - 高度可定制
提供灵活的组件化设计,允许开发者根据需求定制播放器行为。 - 跨平台支持
兼容 Android 4.1+,适用于广泛的设备。 - 错误恢复机制
内置强大的错误处理和恢复机制,确保播放稳定性。
优势
- 高性能:支持硬件加速解码,降低 CPU 负载。
- 灵活性:支持自定义渲染器、数据源和解码器。
- 社区活跃:拥有庞大的开发者社区和丰富的第三方插件。
待优化地方
- 初始化时间较长
在某些情况下,ExoPlayer
的初始化时间可能较长,影响用户体验。 - 内存占用较高
对于低端设备,可能存在内存不足的问题,需要优化资源管理。 - 复杂性较高
配置和使用相对复杂,新手可能需要花费较多时间学习。
三、实现示例
以下是一个完整的媒体文件预览实现示例,包含所有关键代码和资源文件:
1. 主活动 PreviewActivity
java
package com.wangyou.preview;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.viewpager2.widget.ViewPager2;
import java.io.File;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class PreviewActivity extends AppCompatActivity {
private static final String TAG = "PreviewActivity";
private ImageView iv_back, iv_more;
private List<String> media_paths = new ArrayList<>();
private PopupWindow more_info_popup;
private TextView tv_popup_details, tv_popup_file_name, tv_popup_file_path, tv_popup_modified_time, tv_title;
private ViewPager2 view_pager;
private Viewpager2Adapter viewpager2_adapter;
// 初始化
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_preview);
Log.d(TAG, "onCreate called");
initView();
initViewPager();
overridePendingTransition(R.anim.fade_anim_in, R.anim.fade_anim_out);
getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.main_theme_bg));
}
// 释放资源,暂停播放器
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy called");
if (viewpager2_adapter != null) {
viewpager2_adapter.releaseAllPlayers();
}
}
// 暂停播放器
@Override
protected void onPause() {
super.onPause();
Log.d(TAG, "onPause called");
if (viewpager2_adapter != null) {
viewpager2_adapter.pausePlayer();
}
}
// 处理返回键事件,暂停播放器并关闭页面
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
Log.d(TAG, "Back button pressed");
if (viewpager2_adapter != null) {
viewpager2_adapter.pausePlayer();
}
finish();
return true;
}
return super.onKeyDown(keyCode, event);
}
// 关闭页面并添加过渡动画
@Override
public void finish() {
super.finish();
Log.d(TAG, "finish called");
overridePendingTransition(R.anim.fade_anim_in, R.anim.fade_anim_out);
}
// 调整弹出窗口的宽度以适应内容
private void adjustPopupWidth(TextView file_name_text_view, TextView file_path_text_view) {
int file_name_width = (int) file_name_text_view.getPaint().measureText(file_name_text_view.getText().toString());
int file_path_width = (int) file_path_text_view.getPaint().measureText(file_path_text_view.getText().toString());
int screen_width = Utils.getRealScreenWidthPx(this);
int total_width = Math.max(file_name_width, file_path_width);
int default_width = Utils.dpToPx(this, 300);
int max_width = Math.min(total_width, screen_width);
max_width = Math.max(max_width, default_width);
file_name_text_view.setMaxWidth(max_width);
file_path_text_view.setMaxWidth(max_width);
more_info_popup.setWidth(max_width);
}
// 格式化视频时长
private String formatDuration(long duration_millis) {
long hours = duration_millis / 3600000;
long minutes = (duration_millis % 3600000) / 60000;
long seconds = (duration_millis % 60000) / 1000;
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
}
// 格式化文件大小
private String formatFileSize(long size) {
if (size <= 0) return "0";
final String[] units = new String[]{"B", "KB", "MB", "GB", "TB"};
int digit_groups = (int) (Math.log10(size) / Math.log10(1024));
return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digit_groups)) + " " + units[digit_groups];
}
// 从Intent中获取文件路径
private String getFilePathFromIntent(Intent intent) {
if (intent != null) {
String path = intent.getStringExtra("filePath");
if (TextUtils.isEmpty(path)) {
Uri file_uri = intent.getData();
if (file_uri != null) {
return file_uri.getPath();
}
}
return path;
}
return null;
}
// 获取文件详细信息
private String getFileDetails(String file_path) {
if (TextUtils.isEmpty(file_path)) {
return "";
}
File file = new File(file_path);
if (!file.exists()) {
return "";
}
if (Utils.isImage(file_path)) {
return getImageDetails(file);
} else if (Utils.isVideo(file_path)) {
return getVideoDetails(file);
} else if (Utils.isMusic(file_path)) {
return getMusicDetails(file);
}
return "";
}
// 获取文件最后修改时间
private String getFileTime(File file) {
long last_modified = file.lastModified();
Date date = new Date(last_modified);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
// 从Intent中获取文件路径列表
private List<String> getFilePathsWrapper(Intent intent) {
if (intent == null) {
return new ArrayList<>();
}
return intent.getStringArrayListExtra("filePathsWrapper");
}
// 获取视频文件详细信息
private String getVideoDetails(File file) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(file.getAbsolutePath());
String width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
String height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
String duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
long size = file.length();
retriever.release();
return width + "x" + height + " | " + formatFileSize(size) + " | " + formatDuration(Long.parseLong(duration));
}
// 获取图片文件详细信息
private String getImageDetails(File file) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
int width = options.outWidth;
int height = options.outHeight;
long size = file.length();
return width + "x" + height + " | " + formatFileSize(size);
}
// 获取音乐文件详细信息
private String getMusicDetails(File file) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(file.getAbsolutePath());
String duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
long size = file.length();
retriever.release();
return formatFileSize(size) + " | " + formatDuration(Long.parseLong(duration));
}
// 初始化视图组件
private void initView() {
iv_back = findViewById(R.id.iv_preview_back);
tv_title = findViewById(R.id.tv_preview_title);
iv_more = findViewById(R.id.iv_preview_more);
view_pager = findViewById(R.id.preview_viewpager);
iv_back.setOnClickListener(view -> finish());
iv_more.setOnClickListener(v -> showMoreInfoPopup());
}
// 初始化ViewPager
private void initViewPager() {
String file_path = getFilePathFromIntent(getIntent());
Utils.checkMediaPause(file_path);
if (TextUtils.isEmpty(file_path)) {
return;
}
File file = new File(file_path);
if (!file.exists()) {
return;
}
media_paths = getFilePathsWrapper(getIntent());
// 设置初始位置为当前文件
int initial_position = media_paths.indexOf(file.getAbsolutePath());
tv_title.setText(file.getName());
viewpager2_adapter = new Viewpager2Adapter(media_paths, view_pager);
view_pager.setAdapter(viewpager2_adapter);
view_pager.setCurrentItem(initial_position, false);
view_pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
if (media_paths != null && position >= 0 && position < media_paths.size()) {
String current_file_path = media_paths.get(position);
File current_file = new File(current_file_path);
tv_title.setText(current_file.getName());
// 更新 Adapter 的当前播放位置
//fix:Scroll callbacks might be run during a measure & layout pass where you cannot change the RecyclerView data
view_pager.post(() -> {
if (viewpager2_adapter != null) {
viewpager2_adapter.setCurrentPlayingPosition(position);
}
});
}
}
});
}
// 设置弹出窗口的详细信息
private void setPopupViewDetails(View popup_view) {
String current_file_path = media_paths.get(view_pager.getCurrentItem());
File file = new File(current_file_path);
if (!file.exists()) {
return;
}
tv_popup_file_name = popup_view.findViewById(R.id.popup_file_name);
tv_popup_file_path = popup_view.findViewById(R.id.popup_file_path);
tv_popup_modified_time = popup_view.findViewById(R.id.popup_file_modified_time);
tv_popup_details = popup_view.findViewById(R.id.popup_file_details);
tv_popup_file_name.setText(file.getName());
tv_popup_file_path.setText(current_file_path);
tv_popup_modified_time.setText(getFileTime(file));
tv_popup_details.setText(getFileDetails(current_file_path));
adjustPopupWidth(tv_popup_file_name, tv_popup_file_path);
more_info_popup.update();
}
// 显示更多信息弹出窗口
private void showMoreInfoPopup() {
View popup_view = LayoutInflater.from(this).inflate(R.layout.preview_more_info, null);
more_info_popup = new PopupWindow(popup_view, ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT, true);
more_info_popup.setOutsideTouchable(true);
setPopupViewDetails(popup_view);
more_info_popup.showAsDropDown(iv_more);
more_info_popup.setAnimationStyle(R.style.PopupAnimation);
}
private int getRealScreenWidthPx(PreviewActivity context) {
return context.getResources().getDisplayMetrics().widthPixels;
}
private int dpToPx(PreviewActivity context, int dp) {
float density = context.getResources().getDisplayMetrics().density;
return Math.round(dp * density);
}
}
2. ViewPager2 适配器
java
package com.wangyou.preview;
import android.annotation.SuppressLint;
import android.net.Uri;
import android.util.Log;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import com.github.chrisbanes.photoview.PhotoView;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ui.PlayerView;
import java.util.ArrayList;
import java.util.List;
/**
* viewpager适配器,管理图片查看和音视频播放
*/
public class Viewpager2Adapter extends RecyclerView.Adapter<Viewpager2Adapter.MediaViewHolder> {
private final String TAG = "Viewpager2Adapter";
private final List<MediaViewHolder> viewHolders = new ArrayList<>();
private int currentPlayingPosition = -1;
private List<String> mediaPaths; // 更改参数命名
private ViewPager2 viewPager; // 更改参数命名
/**
* 构造函数,初始化媒体路径列表和ViewPager2实例。
* @param mediaPaths 媒体文件路径列表
* @param viewPager ViewPager2实例
*/
public Viewpager2Adapter(List<String> mediaPaths, ViewPager2 viewPager) {
this.mediaPaths = mediaPaths;
this.viewPager = viewPager;
}
/**
* 创建ViewHolder实例。
* @param parent 父视图组
* @param viewType 视图类型
* @return 新创建的ViewHolder实例
*/
@NonNull
@Override
public MediaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
Log.d(TAG, "onCreateViewHolder called");
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.preview_media_item, null);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
view.setLayoutParams(params);
return new MediaViewHolder(view);
}
/**
* 绑定ViewHolder到指定位置的数据。
* @param mediaViewHolder ViewHolder实例
* @param position 数据位置
*/
@Override
public void onBindViewHolder(@NonNull MediaViewHolder mediaViewHolder, int position) {
Log.d(TAG, "onBindViewHolder called with position: " + position);
// 获取媒体路径
String mediaPath = mediaPaths.get(position);
// 根据媒体类型绑定数据
if (Utils.isImage(mediaPath)) {
mediaViewHolder.bindImage(mediaPath);
} else if (Utils.isVideo(mediaPath) || Utils.isMusic(mediaPath)) {
mediaViewHolder.bindVideo(mediaPath);
// 获取ViewHolder的适配器位置
int adapterPosition = mediaViewHolder.getAdapterPosition();
if (adapterPosition != RecyclerView.NO_POSITION) {
// 如果当前项是ViewPager2的当前项,则播放视频
if (adapterPosition == viewPager.getCurrentItem()) {
mediaViewHolder.playVideo();
currentPlayingPosition = adapterPosition;
} else {
// 否则暂停视频
mediaViewHolder.pauseVideo();
}
}
}
// 如果ViewHolder列表中不包含当前ViewHolder,则添加
if (!viewHolders.contains(mediaViewHolder)) {
viewHolders.add(mediaViewHolder);
}
}
@Override
public void onViewDetachedFromWindow(@NonNull MediaViewHolder holder) {
Log.d(TAG, "onViewDetachedFromWindow called");
super.onViewDetachedFromWindow(holder);
// 释放播放器资源
holder.releasePlayer();
}
@Override
public void onViewRecycled(@NonNull MediaViewHolder holder) {
Log.d(TAG, "onViewRecycled called");
super.onViewRecycled(holder);
// 从ViewHolder列表中移除当前ViewHolder
viewHolders.remove(holder);
// 释放播放器资源
holder.releasePlayer();
}
@Override
public int getItemCount() {
return mediaPaths.size();
}
/**
* 设置当前播放位置。
* @param position 播放位置
*/
public void setCurrentPlayingPosition(int position) {
// 记录日志
Log.d(TAG, "setCurrentPlayingPosition called with position: " + position);
// 如果当前播放位置与新位置不同,则更新播放位置
if (currentPlayingPosition != position) {
// 记录之前的播放位置
int previousPosition = currentPlayingPosition;
// 更新当前播放位置
currentPlayingPosition = position;
// 如果之前的位置有效,则通知该位置的项已更改
if (previousPosition != -1) {
notifyItemChanged(previousPosition, "");
}
// 通知当前位置的项已更改
notifyItemChanged(currentPlayingPosition, "");
}
}
public void pausePlayer() {
// 记录日志
Log.d(TAG, "pausePlayer called");
// 遍历所有ViewHolder并暂停播放
for (MediaViewHolder holder : viewHolders) {
if (holder != null && holder.mIsPlaying) {
holder.pauseVideo();
}
}
}
public void playPlayer() {
// 记录日志
Log.d(TAG, "playPlayer called");
// 如果当前播放位置有效且在ViewHolder范围内,则播放视频
if (currentPlayingPosition != -1 && currentPlayingPosition < viewHolders.size()) {
MediaViewHolder holder = viewHolders.get(currentPlayingPosition);
if (holder != null) {
holder.playVideo();
}
}
}
public void releaseAllPlayers() {
// 记录日志
Log.d(TAG, "releaseAllPlayers called");
// 遍历所有ViewHolder并释放播放器资源
for (MediaViewHolder holder : viewHolders) {
if (holder != null) {
holder.releasePlayer();
}
}
// 清空ViewHolder列表
viewHolders.clear();
}
public static class MediaViewHolder extends RecyclerView.ViewHolder {
private final String TAG = "MediaViewHolder";
private final PhotoView photoView;
private final PlayerView playerView;
private ExoPlayer player;
private boolean mIsPlaying;
private ImageButton exoPlayBtn, exoPauseBtn;
private GestureDetector gestureDetector;
/**
* 构造函数,初始化ViewHolder中的视图组件。
* @param itemView 根视图
*/
@SuppressLint("ClickableViewAccessibility")
public MediaViewHolder(@NonNull View itemView) {
super(itemView);
// 初始化PhotoView
photoView = itemView.findViewById(R.id.preview_photo_view);
// 初始化PlayerView
playerView = itemView.findViewById(R.id.preview_player_view);
// 初始化播放按钮
exoPlayBtn = playerView.findViewById(R.id.exo_play);
exoPauseBtn = playerView.findViewById(R.id.exo_pause);
// 初始化手势检测器
gestureDetector = new GestureDetector(itemView.getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
toggleControlsVisibility();
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
togglePlayPause();
exoPlayBtn.setVisibility(View.VISIBLE);
exoPauseBtn.setVisibility(View.GONE);
return true;
}
});
// 设置触摸监听器
playerView.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
}
/**
* 绑定图片到PhotoView。
* @param imagePath 图片路径
*/
void bindImage(String imagePath) {
// 记录日志
Log.d(TAG, "bindImage called with imagePath: " + imagePath);
// 隐藏PlayerView并显示PhotoView
playerView.setVisibility(View.GONE);
photoView.setVisibility(View.VISIBLE);
// 设置图片URI
photoView.setImageURI(Uri.parse(imagePath));
}
/**
* 绑定视频到PlayerView。
* @param videoPath 视频路径
*/
void bindVideo(String videoPath) {
// 记录日志
Log.d(TAG, "bindVideo called with videoPath: " + videoPath);
// 隐藏PhotoView并显示PlayerView
photoView.setVisibility(View.GONE);
playerView.setVisibility(View.VISIBLE);
// 如果是音乐文件,则设置默认艺术作品
if (Utils.isMusic(videoPath)) {
playerView.setDefaultArtwork(ContextCompat.getDrawable(itemView.getContext(), R.drawable.app_icon));
}
// 如果播放器为空,则初始化播放器
if (player == null) {
player = new ExoPlayer.Builder(itemView.getContext()).build();
playerView.setPlayer(player);
initPlayerListener();
}
// 创建媒体项并设置到播放器
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(videoPath));
player.setMediaItem(mediaItem);
player.prepare();
}
public void playVideo() {
// 记录日志
Log.d(TAG, "playVideo called");
// 尝试播放视频
try {
if (player != null) {
player.play();
mIsPlaying = true;
}
} catch (Exception e) {
// 记录异常信息
Log.e(TAG, "Exception in playVideo: " + e.getMessage());
}
}
public void pauseVideo() {
// 记录日志
Log.d(TAG, "pauseVideo called");
// 尝试暂停视频
try {
if (player != null) {
player.pause();
mIsPlaying = false;
}
} catch (Exception e) {
// 记录异常信息
Log.e(TAG, "Exception in pauseVideo: " + e.getMessage());
}
}
void releasePlayer() {
// 记录日志
Log.d(TAG, "releasePlayer called");
// 如果播放器不为空,则释放资源
if (player != null) {
player.stop();
player.clearMediaItems();
player.release();
player = null;
}
}
/**
* 初始化播放器监听器。
*/
private void initPlayerListener() {
// 添加播放器监听器
player.addListener(new Player.Listener() {
@Override
public void onIsPlayingChanged(boolean isPlaying) {
// 记录日志
Log.d(TAG, "onIsPlayingChanged called with isPlaying: " + isPlaying);
// 更新播放状态
mIsPlaying = isPlaying;
}
@Override
public void onPlaybackStateChanged(int playbackState) {
// 记录日志
Log.d(TAG, "onPlaybackStateChanged called with playbackState: " + playbackState);
// 如果播放结束,则更新播放状态
if (playbackState == Player.STATE_ENDED) {
mIsPlaying = false;
}
}
});
}
void togglePlayPause() {
// 记录日志
Log.d(TAG, "togglePlayPause called");
// 如果播放器为空,则直接返回
if (player == null) return;
// 根据当前播放状态切换播放/暂停
if (mIsPlaying) {
player.pause();
} else {
player.play();
}
}
private void toggleControlsVisibility() {
// 记录日志
Log.d(TAG, "toggleControlsVisibility called");
// 根据当前播放状态切换控制按钮的可见性
if (exoPlayBtn.getVisibility() == View.VISIBLE) {
if (mIsPlaying) {
exoPlayBtn.setVisibility(View.GONE);
exoPauseBtn.setVisibility(View.VISIBLE);
} else {
exoPlayBtn.setVisibility(View.VISIBLE);
exoPauseBtn.setVisibility(View.GONE);
}
} else {
if (mIsPlaying) {
exoPauseBtn.setVisibility(View.VISIBLE);
} else {
exoPlayBtn.setVisibility(View.VISIBLE);
}
}
}
}
}
3. 工具类 Utils
java
package com.wangyou.preview;
import android.content.Context;
import android.os.Environment;
import android.util.DisplayMetrics;
import android.webkit.MimeTypeMap;
import java.io.File;
public class Utils {
//判断文件是否为图片
public static boolean isImage(String filePath) {
if (filePath == null || filePath.isEmpty()) {
return false;
}
String name = new File(filePath).getName().toLowerCase();
String[] IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff"};
for (String ext : IMAGE_EXTENSIONS) {
if (name.endsWith(ext)) {
return true;
}
}
return false;
}
//判断文件是否为视频
public static boolean isVideo(String filePath) {
if (filePath == null || filePath.isEmpty()) {
return false;
}
String name = new File(filePath).getName().toLowerCase();
String[] VIDEO_EXTENSIONS = {".mp4", ".avi", ".mkv", ".mov", ".flv", ".wmv", ".mpg", ".mpeg"};
for (String ext : VIDEO_EXTENSIONS) {
if (name.endsWith(ext)) {
return true;
}
}
return false;
}
//判断文件是否为音乐文件
private boolean isMusic(String filePath) {
if (filePath == null || filePath.isEmpty()) {
return false;
}
String name = new File(filePath).getName().toLowerCase();
String[] MUSIC_EXTENSIONS = {".mp3", ".wav", ".aac", ".flac", ".m4a", ".ogg"};
for (String ext : MUSIC_EXTENSIONS) {
if (name.endsWith(ext)) {
return true;
}
}
return false;
}
public static String getMimeType(String path) {
String extension = MimeTypeMap.getFileExtensionFromUrl(path);
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
public static int dpToPx(Context context, int dp) {
return (int) (dp * context.getResources().getDisplayMetrics().density);
}
public static int getRealScreenWidthPx(Context context) {
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
return metrics.widthPixels;
}
//检查打开是是否音视频,发送媒体暂停事件
public static void checkMediaPause(String filePath) {
if (filePath == null || filePath.trim().isEmpty()) {
Log.i(TAG, "checkMediaPause: Invalid file path");
return;
}
boolean isMediaFile = isMusic(filePath) || isVideo(filePath);
Log.i(TAG, "checkMediaPause: isMediaFile = " + isMediaFile);
if (isMediaFile) {
try {
Instrumentation inst = new Instrumentation();
inst.sendKeyDownUpSync(KEYCODE_MEDIA_PAUSE);
} catch (Exception e) {
Log.e(TAG, "checkMediaPause: Failed to send media pause event, e = " + e);
}
}
}
}
四、布局文件
1. activity_preview.xml
xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/main_theme_bg"
android:orientation="vertical">
<!-- 顶部工具栏 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/preview_toolbar"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="20dp">
<!-- 返回按钮 -->
<ImageView
android:id="@+id/iv_preview_back"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/ic_back"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- 标题 -->
<TextView
android:id="@+id/tv_preview_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="middle"
android:maxLines="1"
android:paddingEnd="80dp"
android:textColor="#ffffff"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/iv_preview_back"
app:layout_constraintTop_toTopOf="parent" />
<!-- 更多按钮 -->
<ImageView
android:id="@+id/iv_preview_more"
android:layout_width="30dp"
android:layout_height="30dp"
android:src="@drawable/ic_more"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 分页容器 -->
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/preview_viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/preview_toolbar"
android:layout_marginBottom="20dp" />
</RelativeLayout>
2. 动画资源
res/anim/fade_anim_in.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:fromAlpha="0.0"
android:toAlpha="1.0" />
res/anim/fade_anim_out.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:fromAlpha="1.0"
android:toAlpha="0.0" />
五、AndroidManifest 配置
xml
<manifest>
<!-- 添加必要权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application>
<!-- 预览Activity -->
<activity android:name=".PreviewActivity"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="content" />
<data android:scheme="file" />
<data android:mimeType="audio/*" />
<data android:mimeType="video/*" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
</application>
</manifest>
六、优化建议
-
图片加载优化
结合 Glide 实现图片加载和缓存:
gradleimplementation 'com.github.bumptech.glide:glide:4.12.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
-
ExoPlayer 缓存优化
使用
CacheDataSource
实现媒体缓存:javaCache cache = new SimpleCache(cacheDir, new LeastRecentlyUsedCacheEvictor(100 * 1024 * 1024)); DataSource.Factory cacheDataSourceFactory = new CacheDataSource.Factory() .setCache(cache) .setUpstreamDataSourceFactory(new DefaultDataSource.Factory(context));
-
内存管理
在
onDestroy
中主动释放资源:java@Override protected void onDestroy() { if (player != null) { player.release(); player = null; } super.onDestroy(); }
七、总结
通过结合 PhotoView
和 ExoPlayer
,我们可以低成本地实现高效的媒体文件预览功能。PhotoView
提供了优秀的图片交互体验,而 ExoPlayer
则满足了音视频播放的需求。尽管两者在性能和功能上还有待优化的地方,但它们仍然是目前最主流的选择之一。开发者可以通过以下方式进一步提升体验:
- 性能优化:使用图片压缩、媒体缓存等技术
- 交互增强:添加过渡动画和手势反馈
- 错误处理:完善媒体加载失败的重试机制
- 自适应布局:针对不同屏幕尺寸优化显示效果
希望本文能为开发者提供完整的实现参考和优化思路!