视频外部滤镜

1 使用场景

当 SDK 自带的美颜无法满足需求,例如需要做挂件、贴纸,或者美颜效果无法达到预期时,建议开发者使用外部滤镜功能。

对于比较复杂的场景,例如想要用摄像头画面做图层混合,建议开发者使用外部采集方案,这样性能优化的空间会更大。

2 功能简介

考虑到滤镜的性能问题和美颜厂商的多样性,SDK 的视频外部滤镜采用面向对象设计,结合线程模型,帮助用户把外部代码封装成可替换的滤镜组件。

主要结构如下:

ZegoVideoFilterFactory 是外部滤镜的入口,定义了创建、销毁 ZegoVideoFilter 接口,向 SDK 提供管理 ZegoVideoFilter 生命周期的能力。需要调用 setVideoFilterFactory 的地方必须实现该接口。

ZegoVideoFilter 定义最基本的组件功能,包括 allocateAndStart、stopAndDeAllocate,方便 SDK 在直播流程中进行交互。

请注意,SDK会在适当的时机创建和销毁 ZegoVideoFilter,开发者无需担心生命周期不一致的问题。

3 选择合适的外部滤镜

为了实现传输不同数据模型,适配不同线程模型,同时避免实现多余接口,SDK 采用伪 COM 的设计方式。

开发者需要在子类中显式指定一种数据传递类型,SDK 目前支持的类型有:

滤镜类型

说明

BUFFER_TYPE_MEM

异步滤镜(异步传递 RGBA32 的图像数据)

BUFFER_TYPE_ASYNC_PIXEL_BUFFER

Android 不支持

BUFFER_TYPE_SYNC_PIXEL_BUFFER

Android 不支持

BUFFER_TYPE_SURFACE_TEXTURE

当开发者使用该种类型滤镜时,SDK 会调用此滤镜的 getSurfaceTexture 获取 SurfaceTexture 对象

BUFFER_TYPE_HYBRID_MEM_GL_TEXTURE_2D

异步 RGBA32 的图像数据

BUFFER_TYPE_SYNC_GL_TEXTURE_2D

当开发者使用该种滤镜时,SDK 会调用此滤镜的 onProcessCallback 方法

BUFFER_TYPE_ASYNC_I420_MEM

异步 I420 滤镜(异步传递 I420 的图像数据)

SDK 会根据数据类型,实例化不同类型的 client,在调用 allocateAndStart 时传给外部滤镜。

下面将以 BUFFER_TYPE_MEM 类型滤镜为例演示外部滤镜的用法。

4 创建外部滤镜

4.1 创建外部滤镜工厂

下述代码演示了如何创建外部滤镜工厂。工厂保存了 ZegoVideoFilter 的实例,不会反复创建。

public class VideoFilterFactoryDemo extends ZegoVideoFilterFactory {

private int mode = 0;

private ZegoVideoFilter mFilter = null;

public VideoFilterFactoryDemo(int mode) {

this.mode = mode;

}

public ZegoVideoFilter create() {

if (mFilter == null) {

switch (mode) {

case 0:

mFilter = new VideoFilterMemDemo();

break;

case 1:

mFilter = new VideoFilterSurfaceTextureDemo();

break;

case 2:

mFilter = new VideoFilterHybridDemo();

break;

case 3:

mFilter = new VideoFilterSurfaceTextureDemo2();

break;

case 4:

mFilter = new VideoFilterI420MemDemo();

break;

case 5:

mFilter = new VideoFilterGlTexture2dDemo();

break;

}

}

return mFilter;

}

public void destroy(ZegoVideoFilter vf) {

mFilter = null;

}

}

请注意:

大部分情况下,ZegoVideoFilterFactory 实例会缓存原有 ZegoVideoFilter 实例,开发者需避免创建新的实例。

开发者必须保证 ZegoVideoFilter 在 create 和 destroy 之间是可用的,请勿直接销毁对象。

4.2 设置外部滤镜工厂

开发者需要使用外部滤镜功能时,需在使用前调用 setVideoFilterFactory 设置外部滤镜工厂对象(此例中的对象为步骤 1 中所创建的 VideoFilterFactoryDemo)。

请注意,如果用户释放了工厂对象,不再需要它时,请调用本接口将其设置为空。

if (useVideoFilter) {

// 外部滤镜

if (mFilterFactory == null) {

VideoFilterFactoryDemo videoFilterFactoryDemo = new VideoFilterFactoryDemo(0);

mFilterFactory = videoFilterFactoryDemo;

}

ZegoLiveRoom.setVideoFilterFactory(mFilterFactory);

} else {

ZegoLiveRoom.setVideoFilterFactory(null);

}

4.3 创建外部滤镜

下述代码,以创建BUFFER_TYPE_MEM(异步拷贝图像)类型滤镜为例,开发者可根据需求,参考如下实现步骤。

类定义

ZegoVideoFilterDemo 的类定义如下:

/**

* 异步滤镜设备需要实现 ZegoVideoFilter 协议

*/

public class VideoFilterMemDemo extends ZegoVideoFilter {

@Override

protected void allocateAndStart(Client client) {

...

}

@Override

protected void stopAndDeAllocate() {

...

}

@Override

protected int supportBufferType() {

...

}

@Override

protected synchronized int dequeueInputBuffer(int width, int height, int stride) {

...

}

@Override

protected synchronized ByteBuffer getInputBuffer(int index) {

...

}

@Override

protected synchronized void queueInputBuffer(int bufferIndex, final int width, int height, int stride, long timestamp_100n) {

...

}

...

}

指定滤镜类型

SDK 需要根据外部滤镜 supportBufferType 返回的类型值创建不同的 client 对象。在本示例中,返回 BUFFER_TYPE_MEM:

@Override

protected int supportBufferType() {

return BUFFER_TYPE_MEM;

}

初始化资源

开发者初始化资源在 allocateAndStart 中进行。

开发者在 allocateAndStart 中获取到 client(SDK 内部实现 ZegoVideoFilter.Client 协议的对象),用于通知 SDK 处理结果。

SDK 会在 App 第一次预览/推流/拉流时调用 allocateAndStart。除非 App 中途调用过 stopAndDeAllocate,否则 SDK 不会再调用 allocateAndStart。

@Override

protected void allocateAndStart(Client client) {

mClient = client;

mThread = new HandlerThread("video-filter");

mThread.start();

mHandler = new Handler(mThread.getLooper());

mIsRunning = true;

mProduceQueue.clear();

mConsumeQueue.clear();

mWriteIndex = 0;

mWriteRemain = 0;

mMaxBufferSize = 0;

}

请注意,client 必须保存为强引用对象,在 stopAndDeAllocate 被调用前必须一直被保存。SDK 不负责管理 client 的生命周期。

释放资源

开发者释放资源在 stopAndDeAllocate 中进行。

建议同步停止滤镜任务后再清理 client 对象,保证 SDK 调用 stopAndDeAllocate 后,没有残留的异步任务导致野指针 crash。正常情况下,如果 SDK 是异步调用外部滤镜,外部滤镜完成前处理后,也按照同样的步骤回调 SDK。

@Override

protected void stopAndDeAllocate() {

mIsRunning = false;

final CountDownLatch barrier = new CountDownLatch(1);

mHandler.post(new Runnable() {

@Override

public void run() {

barrier.countDown();

}

});

try {

barrier.await();

} catch (InterruptedException e) {

e.printStackTrace();

}

mHandler = null;

if (Build.VERSION.SDK_INT >= 18) {

mThread.quitSafely();

} else {

mThread.quit();

}

mThread = null;

mClient.destroy();

mClient = null;

}

请注意,开发者必须在 stopAndDeAllocate 方法中调用 client 的 destroy 方法,否则会造成内存泄漏。

SDK 通知外部滤镜当前采集图像的宽高并请求内存池下标

SDK 先调用 ZegoVideoFilter 子类的 int dequeueInputBuffer(int, int, int) 方法,通知外部滤镜当前采集图像的宽高,并请求外部滤镜返回内存池的下标。

@Override

protected synchronized int dequeueInputBuffer(int width, int height, int stride) {

if (stride * height > mMaxBufferSize) {

if (mMaxBufferSize != 0) {

mProduceQueue.clear();

}

mMaxBufferSize = stride * height;

createPixelBufferPool(4);

}

if (mWriteRemain == 0) {

return -1;

}

mWriteRemain--;

return (mWriteIndex + 1) % mProduceQueue.size();

}

SDK 请求外部滤镜返回 Direct 的 ByteBuffer

当 SDK 获得外部滤镜内存池下标后,会调用 ByteBuffer getInputBuffer(int) 获取 Direct 的 ByteBuffer,在 C++ 底层执行内存拷贝。

@Override

protected synchronized ByteBuffer getInputBuffer(int index) {

if (mProduceQueue.isEmpty()) {

return null;

}

ByteBuffer buffer = mProduceQueue.get(index).buffer;

buffer.position(0);

return buffer;

}

外部滤镜处理数据

当 SDK 拷贝完数据后,会调用 void queueInputBuffer(int, int, int, int, long) 方法通知外部滤镜。外部滤镜应当按照约定的数据传递类型,切换线程,异步进行前处理。

此处的演示代码没有做任何操作,只是在另一个线程进行数据拷贝。拷贝流程和 SDK 调用外部滤镜的步骤类似。开发者应该按照各自需求,实现该方法。

@Override

protected synchronized void queueInputBuffer(int bufferIndex, final int width, int height, int stride, long timestamp_100n) {

if (bufferIndex == -1) {

return ;

}

PixelBuffer pixelBuffer = mProduceQueue.get(bufferIndex);

pixelBuffer.width = width;

pixelBuffer.height = height;

pixelBuffer.stride = stride;

pixelBuffer.timestamp_100n = timestamp_100n;

pixelBuffer.buffer.limit(height * stride);

mConsumeQueue.add(pixelBuffer);

mWriteIndex = (mWriteIndex + 1) % mProduceQueue.size();

mHandler.post(new Runnable() {

@Override

public void run() {

if (!mIsRunning) {

Log.e(TAG, "already stopped");

return ;

}

PixelBuffer pixelBuffer = getConsumerPixelBuffer();

int index = mClient.dequeueInputBuffer(pixelBuffer.width, pixelBuffer.height, pixelBuffer.stride);

if (index >= 0) {

ByteBuffer dst = mClient.getInputBuffer(index);

dst.position(0);

pixelBuffer.buffer.position(0);

dst.put(pixelBuffer.buffer);

mClient.queueInputBuffer(index, pixelBuffer.width, pixelBuffer.height, pixelBuffer.stride, pixelBuffer.timestamp_100n);

}

returnProducerPixelBuffer(pixelBuffer);

}

});

}

上述步骤的示例代码可以在目录 app/src/main/java/com/zego/livedemo5/videofilter 下的 VideoFilterFactoryDemo.java 与 VideoFilterMemDemo.java 中找到,具体细节不再赘述。

5 同步滤镜

5.1 创建外部滤镜工厂

下述代码演示了如何创建外部滤镜工厂。工厂保存了 ZegoVideoFilter 的实例,不会反复创建。

public class VideoFilterFactoryDemo extends ZegoVideoFilterFactory {

private int mode = 0;

private ZegoVideoFilter mFilter = null;

public VideoFilterFactoryDemo(int mode) {

this.mode = mode;

}

public ZegoVideoFilter create() {

if (mFilter == null) {

switch (mode) {

case 0:

mFilter = new VideoFilterMemDemo();

break;

case 1:

mFilter = new VideoFilterSurfaceTextureDemo();

break;

case 2:

mFilter = new VideoFilterHybridDemo();

break;

case 3:

mFilter = new VideoFilterSurfaceTextureDemo2();

break;

break;

case 4:

mFilter = new VideoFilterI420MemDemo();

break;

case 5:

mFilter = new VideoFilterGlTexture2dDemo();

break;

}

}

return mFilter;

}

public void destroy(ZegoVideoFilter vf) {

mFilter = null;

}

}

请注意:

大部分情况下,ZegoVideoFilterFactory 实例会缓存原有 ZegoVideoFilter 实例,开发者需避免创建新的实例。

开发者必须保证 ZegoVideoFilter 在 create 和 destroy 之间是可用的,请勿直接销毁对象。

5.2 设置外部滤镜工厂

开发者需要使用外部滤镜功能时,需在使用前调用 setVideoFilterFactory 设置外部滤镜工厂对象(此例中的对象为步骤 1 中所创建的 VideoFilterFactoryDemo)。

请注意,如果用户释放了工厂对象,不再需要它时,请调用本接口将其设置为空。

if (useVideoFilter) {

// 外部滤镜

if (mFilterFactory == null) {

VideoFilterFactoryDemo videoFilterFactoryDemo = new VideoFilterFactoryDemo(5);

mFilterFactory = videoFilterFactoryDemo;

}

ZegoLiveRoom.setVideoFilterFactory(mFilterFactory);

} else {

ZegoLiveRoom.setVideoFilterFactory(null);

}

5.3 创建外部滤镜

下述代码,以创建BUFFER_TYPE_SYNC_GL_TEXTURE_2D(同步拷贝图像)类型滤镜为例,开发者可根据需求,参考如下实现步骤。

类定义

VideoFilterGlTexture2dDemo 的类定义如下:

/**

* 外部滤镜设备需要实现 ZegoVideoFilter 协议

*/

public class VideoFilterMemDemo extends ZegoVideoFilter {

@Override

protected void allocateAndStart(Client client) {

...

}

@Override

protected void stopAndDeAllocate() {

...

}

@Override

protected int supportBufferType() {

...

}

@Override

protected void onProcessCallback(int textureId, int width, int height, long timestamp_100n) {

...

}

...

}

指定滤镜类型

SDK 需要根据外部滤镜 supportBufferType 返回的类型值创建不同的 client 对象。在本示例中,返回 BUFFER_TYPE_SYNC_GL_TEXTURE_2D:

@Override

protected int supportBufferType() {

return BUFFER_TYPE_SYNC_GL_TEXTURE_2D;

}

初始化资源

开发者初始化资源在 allocateAndStart 中进行。

开发者在 allocateAndStart 中获取到 client(SDK 内部实现 ZegoVideoFilter.Client 协议的对象),用于通知 SDK 处理结果。

SDK 会在 App 第一次预览/推流/拉流时调用 allocateAndStart。除非 App 中途调用过 stopAndDeAllocate,否则 SDK 不会再调用 allocateAndStart。

@Override

protected void allocateAndStart(Client client) {

mClient = client;

mWidth = mHeight = 0;

if (mDrawer == null) {

mDrawer = new GlRectDrawer();

}

}

请注意,client 在 stopAndDeAllocate 被调用前必须一直被保存。

释放资源

开发者释放资源在 stopAndDeAllocate 中进行。

建议同步停止滤镜任务后再清理 client 对象,保证 SDK 调用 stopAndDeAllocate 后,没有残留的异步任务导致野指针 crash。正常情况下,如果 SDK 是异步调用外部滤镜,外部滤镜完成前处理后,也按照同样的步骤回调 SDK。

@Override

protected void stopAndDeAllocate() {

if (mTextureId != 0) {

int[] textures = new int[]{mTextureId};

GLES20.glDeleteTextures(1, textures, 0);

mTextureId = 0;

}

if (mFrameBufferId != 0) {

int[] frameBuffers = new int[]{mFrameBufferId};

GLES20.glDeleteFramebuffers(1, frameBuffers, 0);

mFrameBufferId = 0;

}

if (mDrawer != null) {

mDrawer.release();

mDrawer = null;

}

mClient.destroy();

mClient = null;

}

请注意,开发者必须在 stopAndDeAllocate 方法中调用 client 的 destroy 方法,否则会造成内存泄漏。

绘制图像数据

SDK 会通过调用外部滤镜的 void onProcessCallback(int, int, int height, long) 方法通知外部滤镜采集的结果,然后外部滤镜在同一线程进行前处理,再通过调用 client 的 onProgressCallback 通知 SDK 前处理的结果。

@Override

protected void onProcessCallback(int textureId, int width, int height, long timestamp_100n) {

if (mWidth != width || mHeight != height) {

if (mTextureId != 0) {

int[] textures = new int[]{mTextureId};

GLES20.glDeleteTextures(1, textures, 0);

mTextureId = 0;

}

if (mFrameBufferId != 0) {

int[] frameBuffers = new int[]{mFrameBufferId};

GLES20.glDeleteFramebuffers(1, frameBuffers, 0);

mFrameBufferId = 0;

}

mWidth = width;

mHeight = height;

}

if (mTextureId == 0) {

GLES20.glActiveTexture(GLES20.GL_TEXTURE1);

mTextureId = GlUtil.generateTexture(GLES20.GL_TEXTURE_2D);

GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);

mFrameBufferId = GlUtil.generateFrameBuffer(mTextureId);

} else {

GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBufferId);

}

GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

mDrawer.drawRgb(textureId, transformationMatrix,

width, height, 0, 0, width, height);

mClient.onProcessCallback(mTextureId, width, height, timestamp_100n);

}

SDK 不推荐使用这种方式实现外部滤镜,因为在同一线程中,OpenGL ES 的上下文、设置、uniform、attribute 是共用的,倘若对 OpenGL ES 不是很熟悉,极易在细节上出现不可预知的 Bug

上述步骤的示例代码可在 app/src/main/java/com/zego/livedemo5/videofilter 目录下的 VideoFilterFactoryDemo.java 和 VideoFilterGlTexture2dDemo.java 中找到。其它类型外部滤镜的示例代码亦可在 app/src/main/java/com/zego/livedemo5/videofilter 下找到,具体细节不再赘述。

6 Q&A

ZegoVideoFilterFactory的子类什么时候释放?

我们推荐把工厂的实例保存为单例,仅作为 SDK 管理外部滤镜生命周期的通道,开发者可以为工厂子类添加 setter 和 getter,一起管理滤镜的生命周期。

如何使用BUFFER_TYPE_ASYNC_I420_MEM方式传递数据?

BUFFER_TYPE_ASYNC_I420_MEM和BUFFER_TYPE_MEM并无本质区别,只是BUFFER_TYPE_ASYNC_I420_MEM颜色空间是I420,而BUFFER_TYPE_MEM是RGBA32,两者同样都是需要异步实现。

如何使用BUFFER_TYPE_SURFACE_TEXTURE方式传递数据?

选择BUFFER_TYPE_SURFACE_TEXTURE方式时,开发者可以通过client获取SurfaceTexture对象,转换成EglSurface,用于OpenGL ES绘制,同时SDK也要求外部滤镜显式实现ZegoVideoFilter的getSurfaceTexture作为滤镜的输入。

如果对于这个机制不太熟悉,开发者可以参考TextureView.getSurfaceTexture或者MediaCode.createInputSurface。SurfaceTexture作为官方推荐的一种传输数据的管道,在许多系统Api中都有使用,包括android.hardware.camera、android.media.MediaCodec等。

因为没有显式传递图像宽高和时间戳的接口,需要借助SurfaceTexture的setDefaultBufferSize方法设置图像宽高,然后SDK可以通过系统Api获取后续的图像宽高。注意这里的宽高必须和后续的glViewport的宽高保持一致,避免图像变形。具体请参考VideoCaptureFromImage.java。

如何使用BUFFER_TYPE_HYBRID_MEM_GL_TEXTURE_2D方式传递数据?

选择BUFFER_TYPE_HYBRID_MEM_GL_TEXTURE_2D方式时,SDK会按顺序依次调用外部滤镜的dequeueInputBuffer、getInputBuffer、queueInputBuffer方法,向外部滤镜传递数据,而外部滤镜调用client的onProcessCallback向SDK传递包含前处理结果的texture。

请注意:

这里的线程模型是异步的,即SDK调用dequeueInputBuffer、getInputBuffer、queueInputBuffer在SDK线程,外部滤镜前处理调用client的onProcessCallback在外部滤镜的工作线程,同时外部滤镜应确保每次执行OpenGL ES绘制时调用makeCurrent及glViewport,否则会产生不可预知的错误。

开发者必须在 stopAndDeAllocate 方法中,切换到对应的工作线程,再调用 client 的 destroy 方法。因为采用这种方式,SDK会共享线程的上下文,销毁时,如果缺少对应的上下文,可能会出现不可预知的情况。

几种外部滤镜类型在实现上的主要区别是什么?

当数据类型为BUFFER_TYPE_MEM、BUFFER_TYPE_HYBRID_MEM_GL_TEXTURE_2D、BUFFER_TYPE_ASYNC_I420_MEM类型的外部滤镜时,SDK会按照dequeueInputBuffer、getInputBuffer、queueInputBuffer的顺序调用外部滤镜的接口。

BUFFER_TYPE_MEM与BUFFER_TYPE_HYBRID_MEM_GL_TEXTURE_2D类型数据均为RGBA32图像数据;BUFFER_TYPE_ASYNC_I420_MEM类型数据为I420图像数据.

当数据类型为BUFFER_TYPE_SURFACE_TEXTURE类型的外部滤镜时,SDK会调用外部滤镜的getSurfaceTexture方法。

当数据类型为BUFFER_TYPE_SYNC_GL_TEXTURE_2D类型的外部滤镜时,SDK会调用外部滤镜的onProcessCallback方法。

为什么BUFFER_TYPE_MEM这么复杂?

BUFFER_TYPE_MEM完全参考MediaCodec的dequeueInputBuffer

、getInputBuffer、queueInputBuffer的流程实现。因为Android平台没有一个类似于iOS的CVPixelBufferRef的结构体,dequeueInputBuffer和getInputBuffer是为了方便开发者保存每一帧数据故意设计成两个接口,SDK只关心核心的内存拷贝,但是外部滤镜还需要保存图像的宽高。

为什么使用ByteBuffer而不使用byte[]访问内存?

ByteBuffer是Java提供的直接访问C++内存的方法,SDK的核心逻辑是跨平台C++实现的,外部滤镜的实际内存是通过C++管理。为了避免C++堆到Java堆上多余的拷贝,所以选择ByteBuffer。ByteBuffer在Java层可以通过ByteBuffer.allocateDirect方法指定分配内存到C++堆上,在C++层可以通过jenv->NewDirectByteBuffer方法包裹成Java对象,使用起来并不会比byte[]麻烦。同时Android api对ByteBuffer的支持也很友好,比如OpenGL ES里面上传贴图glTexImage2D明确指定需要java.nio.Buffer,Bitmap.copyPixelsFromBuffer也支持java.nio.Buffer。

如何创建SurfaceTexture?

首先当前线程需要attach OpenGL ES上下文,即调用eglMakeCurrent,然后生成类型为GLES11Ext.GL_TEXTURE_EXTERNAL_OES的texture,最后通过SurfaceTexture的构造函数实例化。注意这里的attach OpenGL ES上下文,并没有要求必须是TextureView的回调或者是SurfaceView的回调,开发者完全可以自己创建线程,构造EglContext、EglSurface,和系统控件没有任何联系。具体请参考VideoCaptureFromImage.java实现。

RGBA32和I420的内存是如何排布的?

如何获取摄像头的旋转角度?

当开发者使用美颜厂商提供的SDK时,绝大部分需要指定摄像头的旋转角度。考虑到美颜厂商SDK的兼容性问题,SDK对于任何一种方式传递的图像数据都进行了纠正,即正朝向,开发者不需要关心摄像头旋转多少度。

android mp4添加滤镜,Android 视频外部滤镜:挂件、贴纸,或美颜效果相关推荐

  1. android 视频滤镜,拍视频有滤镜的软件 安卓手机拍摄视频加滤镜,并设置片头片尾文字...

    一年一度的剁手节"双十一"已经过去了,相信很多小伙伴都一夜未眠的疯抢购物.小编那天晚上就早早的进入了睡眠,不和大家拼手速,不占网络:嗯,就是这么乖巧(qiong).在等着快递到来的 ...

  2. android动态添加标签,android – 动态添加Textview

    在布局文件中,我有以下内容: android:layout_width="100dp" android:layout_height="wrap_content" ...

  3. android view添加背景,android – 如何将视图作为背景添加到surfaceView?

    嗨我目前正在制作游戏,其中包含SurfaceView背景中音频效果可视化的视图. surfaceView包含实际的游戏. 我发布了一些代码片段: – main.xml中 android:layout_ ...

  4. android联系人添加公司,android添加联系人(直接添加到联系人数据库)

    添加联系人姓名.手机.头像... java核心代码 package com.kaka.addContact; import java.io.ByteArrayOutputStream; import  ...

  5. android recyclerview添加头部,Android RecyclerView添加Header头部

     Android RecyclerView添加Header头部 Android RecyclerView不像以前的ListView那样直接添加头部,如果要给RecyclerView增加头部,则需要 ...

  6. android studio添加繁体,Android (Android studio3.0.1)一篇可以实现app多语言的转换(简单操作)的教程-Go语言中文社区...

    最近接触到了项目需要,多语言的转换.网上有很多资料,我整理一些,简单适合自己使用的操作. 第一步:打开Android studio 添加 Android Studio插件:AndroidLocaliz ...

  7. android批量添加联系人,Android实现批量添加联系人到通讯录

    由于工作上的需要,把数据库中备份的联系人添加到Android通讯录,一般都有几百条记录,插入一条数据系统默认commit一次,这样效率 由于工作上的需要,把数据库中备份的联系人添加到Android通讯 ...

  8. android应用添加字体,android应用 修改字体

    在androidmanifest 中 theme 中配置 @color/colorPrimary @color/colorPrimaryDark @color/colorAccent @font/po ...

  9. android如何添加gif,Android加载Gif和ImageView的通用解决方案:android-gif-drawable(1)...

     Android加载Gif和ImageView的通用解决方案:android-gif-drawable(1) Android自己的ImageView或者View不能直接加载运行Gif图片,如果要在 ...

最新文章

  1. ^l手动换行符 ^p段落标记符 /n/r_/n
  2. 使用swoole进行消息推送通知,配合vb.net进行客户端开发一样爽[开发篇]
  3. [XPath] XPath 与 lxml (三)XPath 坐标轴
  4. g++ linux 编译开栈_方舟编译器编译hello world踩坑全记录
  5. 【渝粤教育】国家开放大学2018年春季 0064-22T20世纪欧美文学 参考试题
  6. LeetCode 1871. 跳跃游戏 VII(贪心)
  7. 黑客已经盗了 $15,945,221.72 美元!
  8. LINUX下用C遍历一个目录的代码
  9. 中石油邮箱pop3服务器,手机客户端访问中油邮箱设置
  10. ad元件定位孔放在哪一层_机械制造技术(1)——定位误差的计算
  11. obs录制不了Java_simplescreenrecorder(OBS Studio)录屏没有声音最佳解决方案
  12. 三年半经验,蚂蚁头条快手怎么选?网友:第一次看到头条比快手offer低的
  13. Dos用户学Unix指南(1)
  14. 基于51单片机之数码管设计#扩展三位数码管电路,完成0-999递增或递减计数功能,递减间隔200ms左右
  15. 订单(一)——准备订单数据
  16. 浅论信息流广告与DSP营销推广的区别有哪些
  17. Use host networking
  18. linux 电池管理软件,在Linux中下载并安装TLP电源管理工具
  19. 【Code】ASCII码表
  20. CVPR 2018 | 鸡尾酒网络DCTN:源分布结合律引导的迁移学习框架

热门文章

  1. 七段数码管数字时钟实时显示显示(年月日时分秒)
  2. lua_gc 源码学习五
  3. 电磁场仿真——均匀平面波在真空中的三维传播情况的仿真
  4. 附合导线计算软件_AI 可以在 6 小时内设计出计算机芯片?谷歌这项研究厉害了...
  5. 客户关系管理系统的系统需求分析与数据库设计
  6. TOA定位算法的关系与泰勒级数法的原理
  7. MyZip pro专业极速解压放弃了Mac上落后的拖拽解压压缩模式,采用『右键菜单』进行压缩、解压,极大的符合了用户最初的使用习惯。
  8. vue3-ts-cms(项目实战-后台管理系统)(二、登录页面)
  9. 2020世界物联网博览会
  10. the bean ““ could not beinjected as a..........