Android PAG使用方法详解 #3078
-
PAG 动画框架使用方法详解
目录项目集成1. 添加依赖在 dependencies {
implementation 'com.tencent.tav:libpag:latest.release'
}源码参考: 2. 系统要求android {
compileSdkVersion 29
defaultConfig {
minSdkVersion 21
targetSdkVersion 29
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}
3. 权限配置在 <manifest>
<!-- 用于视频导出功能 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REORDER_TASKS" />
<application>
<!-- Your activities -->
</application>
</manifest>源码参考: 核心组件说明主要类介绍
组件关系图8种典型使用场景场景1: 基础动画播放 🎬最简单的PAG动画播放方式 public class SimplePlayActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 1. 创建PAGView
PAGView pagView = new PAGView(this);
pagView.setLayoutParams(new RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
));
// 2. 加载PAG文件(从assets目录)
PAGFile pagFile = PAGFile.Load(getAssets(), "replacement.pag");
// 3. 设置到视图并播放
pagView.setComposition(pagFile);
pagView.setRepeatCount(0); // 0=播放一次, -1=无限循环
pagView.play();
// 4. 添加到布局
setContentView(pagView);
}
}源码参考: 关键API说明:
适用场景:
场景2: 文本替换动态内容 ✏️实现动画中的文本动态替换,如用户名、评论内容等 private void replaceTextDemo() {
// 1. 加载包含文本的PAG文件
PAGFile pagFile = PAGFile.Load(getAssets(), "test2.pag");
// 2. 检查文本图层数量
int textCount = pagFile.numTexts();
Log.d(TAG, "文本图层数量: " + textCount);
if (textCount > 0) {
// 3. 获取第一个文本数据
PAGText textData = pagFile.getTextData(0);
// 4. 修改文本内容
textData.text = "替换后的文本";
// 可选:修改其他属性
// textData.fontSize = 36;
// textData.fillColor = Color.RED;
// 5. 应用修改
pagFile.replaceText(0, textData);
}
// 6. 显示
pagView.setComposition(pagFile);
pagView.play();
}源码参考: PAGText 常用属性: public class PAGText {
public String text; // 文本内容
public float fontSize; // 字体大小
public String fontFamily; // 字体名称
public int fillColor; // 填充颜色
public int strokeColor; // 描边颜色
// ... 更多属性
}适用场景:
场景3: 图片替换 🖼️替换动画中的占位图片,实现动态海报、头像等效果 private PAGImage createPAGImage() {
// 方法1: 从assets加载
InputStream stream = null;
try {
stream = getAssets().open("test.png");
} catch (IOException e) {
e.printStackTrace();
}
Bitmap bitmap = BitmapFactory.decodeStream(stream);
// 方法2: 从资源ID加载
// Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.avatar);
// 方法3: 从网络加载(需先下载到Bitmap)
// Bitmap bitmap = downloadBitmap(imageUrl);
if (bitmap == null) {
return null;
}
return PAGImage.FromBitmap(bitmap);
}
private void replaceImageDemo() {
// 1. 加载PAG文件
PAGFile pagFile = PAGFile.Load(getAssets(), "replacement.pag");
// 2. 检查图片图层数量
int imageCount = pagFile.numImages();
Log.d(TAG, "图片图层数量: " + imageCount);
if (imageCount > 0) {
// 3. 创建PAGImage
PAGImage pagImage = createPAGImage();
if (pagImage != null) {
// 4. 替换指定索引的图片(索引从0开始)
pagFile.replaceImage(0, pagImage);
// 如果有多个图片占位符
// pagFile.replaceImage(1, anotherPAGImage);
}
}
// 5. 显示
pagView.setComposition(pagFile);
pagView.play();
}源码参考: 图片来源方式总结:
适用场景:
场景4: 多文件组合播放 🎭将多个PAG文件组合到同一画布,实现复杂的多层动画效果 private void multipleFilesDemo() {
// 1. 获取屏幕尺寸
WindowManager manager = getWindowManager();
DisplayMetrics metrics = new DisplayMetrics();
manager.getDefaultDisplay().getMetrics(metrics);
int width = metrics.widthPixels;
int height = metrics.heightPixels;
// 2. 创建指定尺寸的画布容器
PAGComposition composition = PAGComposition.Make(width, height);
// 3. 定义单个元素的尺寸
float itemWidth = width / 5f;
float itemHeight = 300;
// 4. 批量添加PAG文件(5列4行,共20个)
for (int i = 0; i < 20; i++) {
int row = i / 5;
int column = i % 5;
// 加载PAG文件
PAGFile pagFile = PAGFile.Load(getAssets(), i + ".pag");
// 创建变换矩阵(缩放和位移)
Matrix matrix = new Matrix();
float scaleX = itemWidth / pagFile.width();
matrix.preScale(scaleX, scaleX); // 缩放
matrix.postTranslate(itemWidth * column, row * itemHeight); // 位移
// 应用变换
pagFile.setMatrix(matrix);
// 可选:设置持续时间(微秒)
pagFile.setDuration(10000000); // 10秒
// 添加到组合
composition.addLayer(pagFile);
}
// 5. 显示
pagView.setComposition(composition);
pagView.play();
}源码参考: Matrix变换说明: Matrix matrix = new Matrix();
// 缩放(以左上角为原点)
matrix.preScale(scaleX, scaleY);
// 旋转(角度)
matrix.postRotate(45); // 旋转45度
// 平移
matrix.postTranslate(dx, dy);
// 应用到PAGFile
pagFile.setMatrix(matrix);适用场景:
场景5: 自定义纹理渲染 (OpenGL集成) 🎮将PAG集成到OpenGL渲染管线,实现游戏引擎集成、自定义特效等 Activity层设置public class TextureDemoActivity extends Activity {
private GLSurfaceView glSurfaceView;
private GLRender glRender;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 1. 创建GLSurfaceView
glSurfaceView = new GLSurfaceView(this);
// 2. 设置OpenGL ES 2.0
glSurfaceView.setEGLContextClientVersion(2);
// 3. 设置自定义渲染器
glRender = new GLRender(this);
glSurfaceView.setRenderer(glRender);
// 4. 设置渲染模式(连续渲染)
glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
setContentView(glSurfaceView);
}
@Override
protected void onResume() {
super.onResume();
glSurfaceView.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
glSurfaceView.onPause();
}
}源码参考: Renderer层实现public class GLRender implements GLSurfaceView.Renderer {
private int textureId;
private PAGPlayer pagPlayer;
private PAGFile pagFile;
private Context context;
private long duration;
private long timestamp = 0;
public GLRender(Context context) {
this.context = context;
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
// 1. 加载PAG文件
pagFile = PAGFile.Load(context.getAssets(), "replacement.pag");
int width = pagFile.width();
int height = pagFile.height();
duration = pagFile.duration();
// 2. 创建OpenGL纹理
textureId = createTexture(width, height);
// 3. 从纹理ID创建PAGSurface
PAGSurface pagSurface = PAGSurface.FromTexture(
textureId,
width,
height
);
// 4. 创建PAGPlayer并配置
pagPlayer = new PAGPlayer();
pagPlayer.setComposition(pagFile);
pagPlayer.setSurface(pagSurface);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
// 1. 计算播放进度
if (timestamp == 0) {
timestamp = System.currentTimeMillis();
}
long playTime = (System.currentTimeMillis() - timestamp) * 1000;
float progress = (playTime % duration) * 1.0f / duration;
// 2. 更新PAG进度并渲染到纹理
pagPlayer.setProgress(progress);
pagPlayer.flush(); // 这里会将PAG内容渲染到textureId
// 3. 使用自定义shader绘制纹理
drawTextureWithCustomShader(textureId);
}
private int createTexture(int width, int height) {
int[] ids = new int[1];
GLES20.glGenTextures(1, ids, 0);
int textureId = ids[0];
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA,
width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
return textureId;
}
private void drawTextureWithCustomShader(int textureId) {
// 自定义shader绘制逻辑
// 详见 GLRender.java 完整实现
}
}源码参考: 关键API说明:
适用场景:
场景6: 时间段裁剪播放 ⏱️只播放PAG文件的指定时间区间,如提取精彩片段 private void timeRangeDemo() {
// 1. 加载PAG文件
PAGFile pagFile = PAGFile.Load(getAssets(), "test2.pag");
// 2. 创建组合容器
PAGComposition composition = PAGComposition.Make(
pagFile.width(),
pagFile.height()
);
// 3. 配置时间裁剪(播放1-3秒的区间)
pagFile.setTimeStretchMode(PAGTimeStretchMode.None); // 不拉伸时间
pagFile.setStartTime(-1000000); // 起始时间:-1秒(微秒单位)
pagFile.setDuration(3000000); // 持续时间:3秒(微秒单位)
// 4. 添加到组合
composition.addLayer(pagFile);
// 5. 显示
pagView.setComposition(composition);
pagView.play();
}源码参考: 时间单位说明: // PAG中的时间单位为微秒(microseconds)
1秒 = 1,000,000 微秒
// 示例:
setStartTime(0); // 从0秒开始
setStartTime(1000000); // 从1秒开始
setStartTime(-1000000); // 从-1秒开始(实际从1秒开始播放)
setDuration(2000000); // 持续2秒
setDuration(500000); // 持续0.5秒时间拉伸模式: // PAGTimeStretchMode 枚举
PAGTimeStretchMode.None // 不拉伸,保持原速
PAGTimeStretchMode.Scale // 缩放时间以适应duration
PAGTimeStretchMode.Repeat // 重复播放以填充duration
PAGTimeStretchMode.RepeatInverted // 往返重复适用场景:
场景7: PAGImageView列表优化 📜在列表或网格中高效展示大量PAG动画 public class MultiplePAGImageViewActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_multiple_pagimageview);
// 批量初始化多个PAGImageView
initMultiplePAGViews();
}
private void initMultiplePAGViews() {
int[] ids = {
R.id.pagView1, R.id.pagView2, R.id.pagView3, R.id.pagView4,
R.id.pagView5, R.id.pagView6, R.id.pagView7, R.id.pagView8,
// ... 更多ID
};
for (int i = 0; i < ids.length; i++) {
initSinglePAGImageView(ids[i], "assets://" + (i + 1) + ".pag");
}
}
private void initSinglePAGImageView(int viewId, String path) {
PAGImageView pagImageView = findViewById(viewId);
// 直接设置路径(支持assets://协议)
pagImageView.setPath(path);
// 设置循环模式
pagImageView.setRepeatCount(-1); // -1 = 无限循环
// 开始播放
pagImageView.play();
}
}源码参考: 布局文件示例: <LinearLayout>
<org.libpag.PAGImageView
android:id="@+id/pagView1"
android:layout_width="100dp"
android:layout_height="100dp" />
<org.libpag.PAGImageView
android:id="@+id/pagView2"
android:layout_width="100dp"
android:layout_height="100dp" />
<!-- 更多PAGImageView -->
</LinearLayout>PAGView vs PAGImageView 对比:
适用场景:
场景8: RecyclerView动画列表 ♻️在RecyclerView中展示PAG动画,实现高性能滚动列表 Activity/Fragment设置public class PAGImageViewListActivity extends FragmentActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 1. 准备数据源(PAG文件路径数组)
String[] paths = new String[22];
for (int i = 0; i < paths.length; i++) {
paths[i] = "assets://" + (i + 1) + ".pag";
}
// 2. 创建Fragment
PAGImageViewRecyclerViewFragment fragment =
new PAGImageViewRecyclerViewFragment();
fragment.setPaths(paths);
// 3. 添加到容器
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
transaction.add(android.R.id.content, fragment);
transaction.commit();
}
}源码参考: RecyclerView Fragment实现public class PAGImageViewRecyclerViewFragment extends Fragment {
private RecyclerView recyclerView;
private CustomAdapter adapter;
private String[] dataset;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.recycler_view_frag, container, false);
// 1. 初始化RecyclerView
recyclerView = rootView.findViewById(R.id.recyclerView);
// 2. 设置LayoutManager(LinearLayout或GridLayout)
RecyclerView.LayoutManager layoutManager =
new LinearLayoutManager(getActivity());
// 或使用GridLayout:
// new GridLayoutManager(getActivity(), 2); // 2列
recyclerView.setLayoutManager(layoutManager);
// 3. 设置Adapter
adapter = new CustomAdapter(dataset);
recyclerView.setAdapter(adapter);
return rootView;
}
public void setPaths(String[] paths) {
this.dataset = paths;
}
}源码参考: Adapter实现public class CustomAdapter extends RecyclerView.Adapter<CustomAdapter.ViewHolder> {
private String[] dataSet;
public static class ViewHolder extends RecyclerView.ViewHolder {
private final PAGImageView pagImageView;
public ViewHolder(View view) {
super(view);
pagImageView = view.findViewById(R.id.pagImageView);
}
public PAGImageView getPAGImageView() {
return pagImageView;
}
}
public CustomAdapter(String[] dataSet) {
this.dataSet = dataSet;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
View view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_pag, viewGroup, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, final int position) {
// 设置PAG动画
PAGImageView pagImageView = viewHolder.getPAGImageView();
pagImageView.setPath(dataSet[position]);
pagImageView.setRepeatCount(-1);
pagImageView.play();
}
@Override
public void onViewRecycled(ViewHolder holder) {
super.onViewRecycled(holder);
// 可选:释放资源
PAGImageView pagImageView = holder.getPAGImageView();
pagImageView.stop();
pagImageView.freeCache();
}
@Override
public int getItemCount() {
return dataSet.length;
}
}源码参考: 列表Item布局<!-- item_pag.xml -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<org.libpag.PAGImageView
android:id="@+id/pagImageView"
android:layout_width="match_parent"
android:layout_height="200dp" />
</androidx.constraintlayout.widget.ConstraintLayout>性能优化建议:
适用场景:
视频导出功能将PAG动画导出为MP4视频文件 完整实现流程public class VideoExporter {
private static final String MIME_TYPE = "video/avc"; // H.264编码
private static final int FRAME_RATE = 30; // 30fps
private static final int IFRAME_INTERVAL = 10; // I帧间隔
private static final int BIT_RATE = 8000000; // 8Mbps码率
private MediaCodec encoder;
private MediaMuxer muxer;
private PAGPlayer pagPlayer;
private PAGFile pagFile;
public void exportToMP4(String outputPath) {
try {
// 1. 准备编码器
prepareEncoder(outputPath);
// 2. 计算总帧数
int totalFrames = (int)(pagFile.duration() * FRAME_RATE / 1000000);
// 3. 逐帧渲染
for (int i = 0; i < totalFrames; i++) {
// 排空编码器缓冲区
drainEncoder(false);
// 渲染当前帧
float progress = i * 1.0f / totalFrames;
pagPlayer.setProgress(progress);
pagPlayer.flush();
Log.d(TAG, "正在导出: " + (i * 100 / totalFrames) + "%");
}
// 4. 完成编码
drainEncoder(true);
} finally {
// 5. 释放资源
releaseEncoder();
}
Log.d(TAG, "导出完成: " + outputPath);
}
private void prepareEncoder(String outputPath) throws IOException {
// 1. 加载PAG文件
pagFile = PAGFile.Load(getAssets(), "replacement.pag");
int width = pagFile.width();
int height = pagFile.height();
// 确保宽高为偶数(H.264要求)
if (width % 2 == 1) width--;
if (height % 2 == 1) height--;
// 2. 配置视频格式
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
// 3. 创建并配置编码器
encoder = MediaCodec.createEncoderByType(MIME_TYPE);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
// 4. 创建PAGSurface(从编码器的InputSurface)
Surface inputSurface = encoder.createInputSurface();
PAGSurface pagSurface = PAGSurface.FromSurface(inputSurface);
// 5. 创建PAGPlayer
pagPlayer = new PAGPlayer();
pagPlayer.setSurface(pagSurface);
pagPlayer.setComposition(pagFile);
pagPlayer.setProgress(0);
// 6. 启动编码器
encoder.start();
// 7. 创建Muxer(MP4合成器)
muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
}
private void drainEncoder(boolean endOfStream) {
final int TIMEOUT_USEC = 10000;
if (endOfStream) {
encoder.signalEndOfInputStream();
}
ByteBuffer[] encoderOutputBuffers = encoder.getOutputBuffers();
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (true) {
int encoderStatus = encoder.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
if (!endOfStream) {
break;
}
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 格式变化时添加轨道并启动muxer
MediaFormat newFormat = encoder.getOutputFormat();
int trackIndex = muxer.addTrack(newFormat);
muxer.start();
} else if (encoderStatus >= 0) {
ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
if (bufferInfo.size != 0) {
// 写入数据到muxer
encodedData.position(bufferInfo.offset);
encodedData.limit(bufferInfo.offset + bufferInfo.size);
muxer.writeSampleData(trackIndex, encodedData, bufferInfo);
}
}
encoder.releaseOutputBuffer(encoderStatus, false);
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
break;
}
}
}
}
private void releaseEncoder() {
if (encoder != null) {
encoder.stop();
encoder.release();
encoder = null;
}
if (muxer != null) {
muxer.stop();
muxer.release();
muxer = null;
}
pagPlayer = null;
}
}源码参考: 使用示例// 在Activity中调用
public void exportVideo(View view) {
// 1. 检查存储权限
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// 申请权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_CODE);
return;
}
// 2. 异步导出(避免阻塞UI)
new Thread(() -> {
String outputPath = Environment.getExternalStorageDirectory()
+ "/pag_export.mp4";
VideoExporter exporter = new VideoExporter();
exporter.exportToMP4(outputPath);
runOnUiThread(() -> {
Toast.makeText(this, "导出成功: " + outputPath,
Toast.LENGTH_LONG).show();
});
}).start();
}导出参数配置
文件大小估算: 适用场景:
进阶技巧1. 性能优化内存管理// ✅ 推荐: 及时释放资源
@Override
protected void onDestroy() {
super.onDestroy();
if (pagView != null) {
pagView.stop();
pagView.freeCache(); // 释放缓存
}
}
// ✅ 推荐: 预加载PAG文件
private PAGFile preloadedFile;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 在后台线程预加载
new Thread(() -> {
preloadedFile = PAGFile.Load(getAssets(), "large.pag");
}).start();
}
// ❌ 避免: 在主线程加载大文件
// PAGFile pagFile = PAGFile.Load(getAssets(), "very_large.pag"); // 可能卡顿列表优化// ✅ 推荐: 列表场景使用PAGImageView
// PAGImageView内存占用约为PAGView的1/3
// ❌ 避免: 在RecyclerView中使用PAGView
// 可能导致内存溢出和滚动卡顿
// ✅ 推荐: 回收不可见的动画
@Override
public void onViewRecycled(ViewHolder holder) {
super.onViewRecycled(holder);
holder.pagImageView.stop();
holder.pagImageView.freeCache();
}
// ✅ 推荐: 按需播放
// 只播放可见区域的动画,使用RecyclerView的滚动监听
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView rv, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
// 滚动停止时才开始播放可见item
playVisibleItems();
}
}
});缓存策略// PAGFile缓存
private static final Map<String, PAGFile> pagFileCache = new HashMap<>();
public static PAGFile getCachedPAGFile(AssetManager assets, String fileName) {
if (!pagFileCache.containsKey(fileName)) {
PAGFile pagFile = PAGFile.Load(assets, fileName);
pagFileCache.put(fileName, pagFile);
}
return pagFileCache.get(fileName);
}
// 注意: 需要在适当时机清理缓存
public static void clearCache() {
pagFileCache.clear();
}2. 播放控制完整的播放控制public class PAGPlayController {
private PAGView pagView;
// 播放/暂停切换
public void togglePlayPause() {
if (pagView.isPlaying()) {
pagView.pause();
} else {
pagView.play();
}
}
// 停止并重置
public void stop() {
pagView.stop();
}
// 跳转到指定进度 (0.0 - 1.0)
public void seekTo(float progress) {
pagView.setProgress(progress);
}
// 获取当前进度
public float getCurrentProgress() {
return pagView.getProgress();
}
// 设置播放速度(需要自定义实现)
public void setPlaybackSpeed(float speed) {
// PAG暂不直接支持变速,可通过控制刷新频率实现
}
}播放监听pagView.addListener(new PAGView.PAGViewListener() {
@Override
public void onAnimationStart(PAGView view) {
Log.d(TAG, "动画开始");
}
@Override
public void onAnimationEnd(PAGView view) {
Log.d(TAG, "动画结束");
// 可以在这里做清理或跳转
}
@Override
public void onAnimationCancel(PAGView view) {
Log.d(TAG, "动画取消");
}
@Override
public void onAnimationRepeat(PAGView view) {
Log.d(TAG, "动画重复播放");
}
@Override
public void onAnimationUpdate(PAGView view) {
// 每帧更新回调
float progress = view.getProgress();
// 更新UI(如进度条)
}
});循环模式// 播放一次
pagView.setRepeatCount(0);
// 播放3次
pagView.setRepeatCount(2); // 初始播放1次 + 重复2次 = 总共3次
// 无限循环
pagView.setRepeatCount(-1);
// 往返循环
pagView.setRepeatCount(-1);
// 注: PAG暂不直接支持往返模式,可通过监听onAnimationRepeat实现3. 文件路径支持PAG支持多种文件加载方式: // 方式1: 从assets目录加载
PAGFile pagFile1 = PAGFile.Load(getAssets(), "animation.pag");
// 方式2: 从文件系统路径加载
String filePath = "/sdcard/Download/animation.pag";
PAGFile pagFile2 = PAGFile.Load(filePath);
// 方式3: 从字节数组加载
byte[] bytes = readFileToBytes("animation.pag");
PAGFile pagFile3 = PAGFile.Load(bytes, bytes.length);
// 方式4: PAGImageView支持路径协议
PAGImageView pagImageView = findViewById(R.id.pagImageView);
// assets协议
pagImageView.setPath("assets://animation.pag");
// file协议
pagImageView.setPath("file:///sdcard/animation.pag");
// 直接路径(会自动识别)
pagImageView.setPath("/sdcard/animation.pag");4. 动态属性修改// 获取文件信息
PAGFile pagFile = PAGFile.Load(getAssets(), "test.pag");
int width = pagFile.width();
int height = pagFile.height();
long duration = pagFile.duration(); // 微秒
double frameRate = pagFile.frameRate();
// 修改透明度
pagFile.setAlpha(0.5f); // 0.0-1.0
// 修改变换矩阵(缩放、旋转、平移)
Matrix matrix = new Matrix();
matrix.postScale(0.5f, 0.5f); // 缩小50%
matrix.postRotate(45); // 旋转45度
matrix.postTranslate(100, 100); // 平移(100, 100)
pagFile.setMatrix(matrix);
// 修改时间范围
pagFile.setStartTime(1000000); // 从1秒开始
pagFile.setDuration(2000000); // 持续2秒
// 获取图层信息
int textCount = pagFile.numTexts();
int imageCount = pagFile.numImages();
int layerCount = pagFile.numChildren();5. 错误处理public class PAGErrorHandler {
// 安全加载PAG文件
public static PAGFile loadPAGFileSafely(AssetManager assets, String fileName) {
try {
PAGFile pagFile = PAGFile.Load(assets, fileName);
if (pagFile == null) {
Log.e(TAG, "加载失败: " + fileName);
return null;
}
// 验证文件有效性
if (pagFile.width() <= 0 || pagFile.height() <= 0) {
Log.e(TAG, "无效的PAG文件: " + fileName);
return null;
}
return pagFile;
} catch (Exception e) {
Log.e(TAG, "加载异常: " + fileName, e);
return null;
}
}
// 检查文件兼容性
public static boolean checkCompatibility(PAGFile pagFile) {
// 检查是否包含不支持的特性
// 可以根据实际需求添加检查逻辑
return true;
}
// 降级方案
public static void loadWithFallback(PAGView pagView, String primaryFile,
String fallbackFile) {
PAGFile pagFile = loadPAGFileSafely(getAssets(), primaryFile);
if (pagFile == null) {
Log.w(TAG, "使用降级文件: " + fallbackFile);
pagFile = loadPAGFileSafely(getAssets(), fallbackFile);
}
if (pagFile != null) {
pagView.setComposition(pagFile);
pagView.play();
} else {
// 显示错误UI或占位图
showErrorPlaceholder(pagView);
}
}
}6. 线程安全// ✅ 正确: PAGView操作在主线程
runOnUiThread(() -> {
pagView.play();
pagView.setProgress(0.5f);
});
// ✅ 正确: PAGFile加载可在子线程
new Thread(() -> {
PAGFile pagFile = PAGFile.Load(getAssets(), "large.pag");
runOnUiThread(() -> {
pagView.setComposition(pagFile);
pagView.play();
});
}).start();
// ✅ 正确: PAGPlayer在有GLContext的线程
// 如GLSurfaceView的渲染线程
@Override
public void onDrawFrame(GL10 gl) {
pagPlayer.setProgress(progress);
pagPlayer.flush();
}
// ❌ 错误: 在子线程操作PAGView
new Thread(() -> {
pagView.play(); // 可能崩溃
}).start();项目结构核心文件说明
快速开始1. 导入项目# 克隆或下载项目
git clone https://github.com/Tencent/pag-android.git
# 使用Android Studio打开
# File -> Open -> 选择 pag-android-master/sample 目录2. 同步依赖// 确保项目级build.gradle配置正确
allprojects {
repositories {
mavenCentral()
google()
}
}
// 确保模块级build.gradle包含PAG依赖
dependencies {
implementation 'com.tencent.tav:libpag:latest.release'
}等待Gradle同步完成。 3. 运行Demo
4. 查看源码重点阅读以下文件了解核心用法: 5. 集成到自己的项目最简单的集成步骤:// Step 1: 添加依赖到build.gradle
implementation 'com.tencent.tav:libpag:latest.release'
// Step 2: 布局文件中添加PAGView
<org.libpag.PAGView
android:id="@+id/pagView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
// Step 3: Activity中初始化
PAGView pagView = findViewById(R.id.pagView);
PAGFile pagFile = PAGFile.Load(getAssets(), "animation.pag");
pagView.setComposition(pagFile);
pagView.play();注意事项1. 线程安全
|
| 场景类型 | 推荐方案 | Demo参考 |
|---|---|---|
| 启动页/引导页 | PAGView | 场景1 |
| 个性化动画 | PAGView + 文本/图片替换 | 场景2/3 |
| 列表特效 | PAGImageView + RecyclerView | 场景7/8 |
| 礼物墙 | PAGImageView批量展示 | 场景6 |
| 游戏集成 | PAGPlayer + OpenGL | 场景4 |
| 视频生成 | PAGPlayer + MediaCodec | 视频导出 |
学习路径建议
入门 (1天)
└─ 运行Demo项目
└─ 学习场景1: 基础播放
└─ 学习场景2: 文本替换
进阶 (2-3天)
└─ 学习场景3: 图片替换
└─ 学习场景7/8: 列表集成
└─ 学习视频导出功能
高级 (1周)
└─ 学习场景4: OpenGL集成
└─ 学习场景5: 时间裁剪
└─ 学习场景6: 多文件组合
└─ 性能优化实践
附录
时间单位转换
// PAG使用微秒(microseconds)作为时间单位
1秒 = 1,000,000 微秒
// 常用转换
public class TimeUtil {
// 秒 -> 微秒
public static long secondsToMicros(double seconds) {
return (long)(seconds * 1_000_000);
}
// 毫秒 -> 微秒
public static long millisToMicros(long millis) {
return millis * 1_000;
}
// 微秒 -> 秒
public static double microsToSeconds(long micros) {
return micros / 1_000_000.0;
}
}颜色格式
// PAG颜色使用ARGB格式(Android标准)
int color = Color.argb(255, 255, 0, 0); // 红色
// 或使用16进制
int color = 0xFFFF0000; // 红色 (AARRGGBB)
// 设置文本颜色
PAGText textData = pagFile.getTextData(0);
textData.fillColor = Color.RED;常用尺寸
// 获取屏幕尺寸
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
int screenWidth = metrics.widthPixels;
int screenHeight = metrics.heightPixels;
// 设置PAG尺寸
PAGComposition composition = PAGComposition.Make(screenWidth, screenHeight);文档版本: v1.0
最后更新: 2025-01-12
基于项目: pag-android-master (June 2024)
问题反馈
如有任何疑问或建议,请访问:
Happy Coding! 🎉
Beta Was this translation helpful? Give feedback.
Replies: 5 comments 8 replies
-
|
1、PAGView vs PAGImageView 对比 2、PAGView 和 PAGImageView 的接口默认都是可以在子线程执行的,除了系统层面要求的接口如 setFrame 等涉及到渲染尺寸的设置,而不是要求在主线程,内部实现层面,渲染都是在子线程,只有上屏才会切换主线程,这个 SDK 内部已经实现了 |
Beta Was this translation helpful? Give feedback.
-
|
最新版本4.5.2的PagImageView中没有freeCache()释放,还有什么方法可以释放的,我这边生产环境有有死锁的问题导致崩溃,有什么办法处理吗 |
Beta Was this translation helpful? Give feedback.
-
|
备注下:这是是非官方文档,中间有部分内容有些问题,仅供大家参考,使用层面官方文档:https://pag.io/ 防止大家理解出现偏差 |
Beta Was this translation helpful? Give feedback.
-
|
android 平台 4.5.2 及以后的版本中 TextureDemoActivity 是无法显示的 |
Beta Was this translation helpful? Give feedback.
-
|
我使用一个PagView 播放不同的pagfile 有什么好的方案,在播放中可能会切换到另一个,或者这个播放完后播放另一个,当始终都只会播放一个 |
Beta Was this translation helpful? Give feedback.
1、PAGView vs PAGImageView 对比
在使用场景层面,如果涉及到文本编辑、占位图替换、实色图层修改颜色和全屏播放中的一种,不推荐使用 PAGImageView, 同时推荐PAGImageView 使用 setPath 的加载方式,否则性能可能不如 PAGView,具体原因看这篇文档:https://pag.io/docs/use-pagimageview.html , 核心是要理解 PAGImageView 和 PAGView 的实现差异,对于 PAGImageView 而言,只有渲染结果缓存下来才有价值,如果缓存不下来还不如使用 PAGView, PAGImageView 适用于渲染固定的内容,且缓存内容不是太大(渲染尺寸不易过大、播放时长不是太长,默认占用的磁盘缓存大小为 1 GB,如果缓存超过阈值失效意义也很有限),这样每次都可以读取缓存
2、PAGView 和 PAGImageView 的接口默认都是可以在子线程执行的,除了系统层面要求的接口如 setFrame 等涉及到渲染尺寸的设置,而不是要求在主线程,内部实现层面,渲染都是在子线程,只有上屏才会切换主线程,这个 SDK 内部已经实现了