Android视频裁剪自定义View
来源:互联网 发布:淘宝售后安装平台接单 编辑:程序博客网 时间:2024/06/05 04:59
功能需求:
有个视频裁剪功能,需要自定义View具体如下
1. 裁剪选择区域模块,可以自定义最少裁剪时间
2. 当选择低于最少裁剪时间时,再次滑动会自动推动左右选择轴,直至碰到边缘为止
3. 选择空白区域,并左右滑动时自动推动选择轴的位置
4. 可选控制(是否裁剪模式,是否显示播放进度,裁剪模式下(未选中的背景增加阴影图层),播放过的背景增加阴影图层等)
实现代码:
package com.play.pro.widgets;import java.io.File;import java.util.Timer;import java.util.TimerTask;import com.play.pro.R;import com.play.pro.utils.ScreenUtils;import android.content.Context;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.media.MediaMetadataRetriever;import android.media.ThumbnailUtils;import android.os.Handler;import android.os.Message;import android.text.TextUtils;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;/** * 视频(缩略图、裁剪)进度滑动条 */public class VideoSeekBar extends View { /** 日志Tag */ //private final String TAG = "VideoSeekBar"; // ============== 其他变量 =============== /** 滑动的图片(左右两个) */ private Bitmap leftBitmap, rightBitmap; // -- /** dip转换px */ private int dip = 0; /** 视频路径 */ private String videoUri = null; /** 当前View 宽度 */ private int vWidth; /** 当前View 高度 */ private int vHeight; /** 关键帧时间 */ private float videoFrame = 0f; /** 视频的总长度(毫秒) */ private int videoDuration = 0; /** 当前播放的时间(毫秒) */ private int videoPlayProgress = 0; /** 屏幕上坐标转换时间 - X轴 横 */ private float xTime = -1f; // -- /** 是否裁剪模式 */ private boolean isCutMode = false; /** 是否清空内存 - 销毁资源*/ private boolean isClearMemory = true; /** 是否绘制播放进度条 */ private boolean isDrawProgressLine = false; /** 是否绘制播放进度背景 */ private boolean isDrawProgressBG = false; // -- 画笔 -- /** 绘制缩略图画笔 */ private Paint thumbPaint = new Paint(); /** 播放进度画笔 */ private Paint progressPaint = new Paint(); /** 播放进度背景(阴影层)画笔 */ private Paint progressBgPaint = new Paint(); // ============== 缩略图处理 =============== /** 缩略图数量 */ private int thumbCount = 7; /** 缩略图Bitmap */ private Bitmap[] thumbBitmaps; public VideoSeekBar(Context context) { super(context); init(); } public VideoSeekBar(Context context, AttributeSet attrs) { super(context, attrs); init(); } public VideoSeekBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // -------------------- // 绘制缩略图 - 防止进行销毁中,导致触发 if (!isClearMemory && thumbBitmaps != null) { // 遍历缩略图数量 for (int i = 0;i < thumbCount;i++) { if(isClearMemory){ // 如果正在回收中,则跳出方法 break; } if (thumbBitmaps[i] != null) { try { // 绘制缩略图 canvas.drawBitmap(thumbBitmaps[i], i * thumbBitmaps[i].getWidth(), 0, thumbPaint); } catch (Exception e) { } } } } // -------------------- // 是否绘制播放进度背景(阴影层) - 裁剪模式下滑动也会有这个阴影,所以需要加上是否裁剪模式处理 if (!isCutMode && isDrawProgressBG){ // 计算时间防止等于-1 、 防止获取高度失败 if (getViewWidthConvertTime() != -1f && vHeight != 0){ // 转换当前的X轴位置(播放进度 / 每个X轴对应的时间) float convX = ((float) videoPlayProgress) / xTime; // 如果大于等于View的宽度重新设置 if(convX + dip >= vWidth){ // 重置位置,直接到结尾 convX = vWidth - (int) (dip * 1.5); } // 绘制一个矩形 canvas.drawRect(0, 0, convX + (dip / 2), vHeight, progressBgPaint); } } // -------------------- // 是否绘制播放进度条 if (isDrawProgressLine){ // 计算时间防止等于-1 、 防止获取高度失败 if (getViewWidthConvertTime() != -1f && vHeight != 0){ // 转换当前的X轴位置(播放进度 / 每个X轴对应的时间) float convX = ((float) videoPlayProgress) / xTime; // 如果大于等于View的宽度重新设置,防止线条回弹(vWidth - convX < dip 导致下次会大于vWidth,线条会回弹) if(convX + dip >= vWidth){ // 重置位置,直接到结尾,显示一条线 convX = vWidth - (int) (dip * 1.5); } // 绘制一个矩形(一条线) canvas.drawRect(convX, 0, convX + (dip / 2), vHeight, progressPaint); } } // -------------------- // 判断是否裁剪模式, 并且高度不等于0,防止计算出现问题 if (isCutMode && vHeight != 0){ // 计算右边边距值 reckonRightSX(); // 绘制左边滑动的X轴位置 canvas.drawBitmap(leftBitmap, leftSX, 0, thumbPaint); // 绘制右边滑动的X轴位置 canvas.drawBitmap(rightBitmap, rightSX, 0, thumbPaint); // === 绘制左边拖动阴影图层 === if (leftSX != 0f){ // 绘制一个矩形 canvas.drawRect(0, 0, leftSX, vHeight, progressBgPaint); } // === 绘制右边拖动阴影图层 === if (rightSX != rightMarginX){ // 绘制一个矩形 canvas.drawRect(rightSX + sliderIgWidth, 0, vWidth, vHeight, progressBgPaint); } } } // ===================== // -- /** 右边的边距(间距图片宽度) */ private float rightMarginX = 0f; /** 左边滑动的X轴 */ private float leftSX = 0f; /** 右边滑动的X轴 */ private float rightSX = 0f; /** 滑动的图片宽度 */ private int sliderIgWidth = 0; // === /** 上次滑动的值 */ private float oTouchX = -1f; /** 旧的中间值 */ private float lrMiddleX = -1f; /** 滑动的View*/ private int touchView = -1; /** 滑动左边的View */ private final int TOUCH_LEFT_VIEW = 1; /** 滑动右边的View */ private final int TOUCH_RIGHT_VIEW = 2; /** 滑动左右两边中间空白部分 */ private final int TOUCH_MIDST_VIEW = 3; @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); // 属于裁剪模式才进行处理 if(isCutMode){ // 滑动中的X轴位置 float xMove = event.getX(); // -- switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 按下时 // 这样判断是刚好在之间,为了增加触摸体验,增加多一般的边距触摸优化 //if (xMove >= leftSX && xMove <= (leftSX + sliderIgWidth)) // -- if (xMove >= (leftSX - sliderIgWidth / 2) && xMove <= (leftSX + ((float) sliderIgWidth) * 1.5)){ touchView = TOUCH_LEFT_VIEW; // 计算滑动距离 reckonSlide(xMove); } else if (xMove >= (rightSX - sliderIgWidth / 2) && xMove <= (rightSX + ((float) sliderIgWidth) * 1.5)){ touchView = TOUCH_RIGHT_VIEW; // 计算滑动距离 reckonSlide(xMove); } else if (xMove >= (leftSX + sliderIgWidth) && xMove <= (rightSX + sliderIgWidth)){ // 属于滑动两个View中间模块 touchView = TOUCH_MIDST_VIEW; // 计算滑动距离 reckonSlide(xMove); } else { // 表示都没操作 lrMiddleX = oTouchX = touchView = -1; } break; case MotionEvent.ACTION_MOVE: // 滑动中 // 计算滑动距离 reckonSlide(xMove); break; case MotionEvent.ACTION_UP: // 抬起时 lrMiddleX = oTouchX = touchView = -1; break; } } return true; } /** 计算右边的值 */ private void reckonRightSX(){ if(rightMarginX == 0f){ rightMarginX = vWidth - sliderIgWidth; } if(rightSX == 0f){ // 默认值为0则表示为最尾端 rightSX = rightMarginX; } } /** * 计算滑动 * @param xMove 滑动的X轴 */ private void reckonSlide(float xMove){ // 计算右边边距值 reckonRightSX(); // 如果都不属于滑动,则不处理 if(!(touchView == TOUCH_LEFT_VIEW || touchView == TOUCH_RIGHT_VIEW || touchView == TOUCH_MIDST_VIEW)){ return; } // 转换关键帧相差的X轴位置(关键帧时间 / 每个X轴对应的时间) float convX = videoFrame / xTime; // 计算间隔宽度(判断是滑动图片宽度大还是关键帧宽度大) float spacing = (convX > sliderIgWidth) ? convX : sliderIgWidth; // -- if(touchView == 1){ // 属于滑动左边图片 // 虚拟位置 = 滑动位置 + 间距宽度 float vX = xMove + spacing; // 判断是否滑动会推动到右边 if(vX > rightSX){ // 如果已经给推到边缘了,则进行控制 if (rightSX >= rightMarginX){ // 设置右边到边缘 rightSX = rightMarginX; // 左边 = 右边 - 间距宽度(防止重叠) leftSX = rightSX - spacing; } else { // 如果不在边缘,则进行推 leftSX = xMove; rightSX = xMove + spacing; } } else { // 如果小于则表示没有触碰到 leftSX = xMove; // 如果边距小于一半则滑动到底部 + 3分之1的边距 if (xMove <= sliderIgWidth / 2 + sliderIgWidth / 3){ leftSX = 0f; } } // 最后再进行判断多一次,防止出现意外(快速滑动) adjustLoc(TOUCH_LEFT_VIEW, spacing); } else if (touchView == 2){ // 属于滑动右边图片 // 判断是否滑动到边缘(右侧边缘) if(xMove >= rightMarginX){ // 滑动到边缘则直接设置边缘 rightSX = rightMarginX; } else { // 判断是否触碰到左边 -> 滑动的距离 - 左边的位置 > 边距,表示没触碰 if (xMove - leftSX > spacing){ rightSX = xMove; } else { // 如果触碰了 if (leftSX <= 0){ // 如果左边已经到了边缘 // 设置左边到边缘 leftSX = 0f; // 右边 = 间距宽度 rightSX = spacing; } else { // 左边没到边缘,则进行推 rightSX = xMove; leftSX = rightSX - spacing; } } } // 最后再进行判断多一次,防止出现意外(快速滑动) adjustLoc(TOUCH_RIGHT_VIEW, spacing); } else if (touchView == 3){ // 属于滑动两个View 中间空白的 // 左右两个的间隔 = 右边减去左边(左边坐标 + 图片宽度) float lrSpace = rightSX - leftSX; if (lrMiddleX == -1f){ // 获取中间值 lrMiddleX = lrSpace; } // 判断滑动方向 if (oTouchX == -1f){ // 记录上次的滑动值 oTouchX = xMove; return; } if (lrMiddleX > 0){ // 判断左边是否已经到达最右边 if(rightSX > rightMarginX){ // 如果已经给推到边缘了,则进行控制 adjustLoc(TOUCH_MIDST_VIEW, lrMiddleX); // 调整位置 } else if (leftSX < 0){ // 如果左边的距离等于0 adjustLoc(TOUCH_MIDST_VIEW, lrMiddleX); // 调整位置 } else { // 同步位移 // 判断滑动方向 if (xMove > oTouchX){ // 往右边滑动 if (rightSX < rightMarginX){ rightSX = rightSX + (xMove - oTouchX); // -- leftSX = rightSX - lrMiddleX; } } else if (xMove < oTouchX){ // 往左边滑动 if (leftSX > 0){ leftSX = leftSX - (oTouchX - xMove); // -- rightSX = leftSX + lrMiddleX; } } // 记录上次的滑动值 oTouchX = xMove; // 调整位置 adjustLoc(TOUCH_MIDST_VIEW, lrMiddleX); } } } // 进行绘制 invalidate(); } /** * 调整位置(防止左右超出边缘边距) * @param touchView 滑动的View * @param spacing 两个View间隔的边距 */ private void adjustLoc(int touchView, float spacing){ // 判断左边是否到达边缘 if (leftSX <= 0){ // 设置左边到边缘 leftSX = 0f; // 右边 = 间距宽度 float tRightSX = spacing; // 判断当前位置是否大于边距 if (rightSX < tRightSX){ rightSX = tRightSX; } } // -- // 判断右边是否到达边缘 if (rightSX >= rightMarginX){ // 设置右边到边缘 rightSX = rightMarginX; // 左边 = 右边 - 间距宽度(防止重叠) float tLeftSX = rightSX - spacing; // 判断当前位置是否大于计算出来的位置 if (leftSX > tLeftSX){ leftSX = tLeftSX; } } } // =========================================== /** * 初始化操作 */ private void init(){ // 防止不进行绘画 触发onDraw setWillNotDraw(false); // 获取左右两个滑动的图片 leftBitmap = BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.ic_slider_left); rightBitmap = BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.ic_slider_right); // 先保存滑动图片宽度 sliderIgWidth = leftBitmap.getWidth(); // 1 dip 对应的px dip = ScreenUtils.dipConvertPx(getContext(), 1.0f); // 初始化画笔 initPaint(); } /** * 初始化画笔 */ private void initPaint(){ // 初始化画笔 thumbPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 缩略图 progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 播放进度 白色竖直线条 progressBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 播放进度背景,半透明(画布遮挡层) // 画笔颜色 progressPaint.setColor(Color.rgb(255, 255, 255)); // 字体颜色 - 白色 progressBgPaint.setColor(Color.rgb(0, 0, 0)); // 背景进度颜色(画布遮挡层) // 设置透明度 progressBgPaint.setAlpha(60); // 画布遮挡层 // 设置画笔大小 progressPaint.setStrokeWidth(dip * 2); // 线条 // 设置画笔样式 progressPaint.setStyle(Paint.Style.STROKE); // 设置粗线 - 线条 } // ============== 内部计算方法 ============== /** * 获取View的宽度,转换对应的坐标值 = 时间 * @return */ private float getViewWidthConvertTime(){ if(xTime == -1f){ if(vWidth != 0 && videoDuration != 0){ // 视频总进度 / 宽度 = 每个坐标占用多少毫秒 xTime = videoDuration / vWidth; } } return xTime; } /** * 获取滑动图片 * @param isLeft * @return */ private Bitmap getSliderBitmap(boolean isLeft){ // 防止高度为0 if(vHeight != 0){ // 获取高度进行计算 int bHeight = leftBitmap.getHeight(); // 判断是否需要缩放,高度不一直则要求缩放 if (bHeight != vHeight){ // 获取图片宽度 int bWidth = leftBitmap.getWidth(); // 计算宽度比例 bWidth = (int) (((float) vHeight / (float) bHeight) * bWidth); // 保存缩放比例后的宽度 sliderIgWidth = bWidth; // 进行比例缩放图片 leftBitmap = Bitmap.createScaledBitmap(leftBitmap, bWidth, vHeight, true); rightBitmap = Bitmap.createScaledBitmap(rightBitmap, bWidth, vHeight, true); } } return isLeft ? leftBitmap : rightBitmap; } // ============== 内部处理方法 ============== /** 回收缩略图内存 */ private void clearThumbs(){ // 销毁资源中 isClearMemory = true; // -- if(thumbBitmaps != null){ for(int i = 0, c = thumbBitmaps.length;i < c;i++){ Bitmap bitmap = thumbBitmaps[i]; if(bitmap != null){ if(bitmap != null && !bitmap.isRecycled()){ try { bitmap.recycle(); } catch (Exception e) { } } bitmap = null; } } } } /** 创建缩略图 */ private void buildThumbs(){ // 先回收旧的内容 clearThumbs(); // 重新绘制进行刷新 postInvalidate(); // 判断路径是否为null if(!TextUtils.isEmpty(videoUri)){ // 开启后台线程,生成缩略图 new Thread(btRunn).start(); } } /** 生成缩略图(来自本地视频) */ private void buildThumbsToLocal(){ // 进行创建缩略图(非回收) isClearMemory = false; // 设置Media构造器 MediaMetadataRetriever mediaRetriever = new MediaMetadataRetriever(); try { // 防止两个都为默认值 while(vWidth == 0 || vHeight == 0){ vWidth = getWidth(); vHeight = getHeight(); } // 计算每个图片的宽度(宽度 / 总数) int btWidth = vWidth / thumbCount; // 图片的高度 int btHeight = vHeight; // 设置视频的路径 mediaRetriever.setDataSource(videoUri); // 取得视频的长度(单位为毫秒) String vTime = mediaRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); // 保存视频总长度(毫秒) setVideoDuration(Integer.valueOf(vTime)); // 进行计算滑动的边距 getSliderBitmap(true); // 计算右边边距值 reckonRightSX(); // 获取View的宽度,转换对应的坐标值 = 时间 getViewWidthConvertTime(); // 转换时间,然后平分,设置缩略图时间间隔 int interValSec = Integer.valueOf(vTime) / thumbCount; // 初始化缩略图容器 thumbBitmaps = new Bitmap[thumbCount]; // 遍历生成缩略图 for (int i = 0;i < thumbCount;i++) { // 计算时间(秒数) long timeUs = i * interValSec * 1000; // 获取生成缩略图 Bitmap bitmap = mediaRetriever.getFrameAtTime(timeUs, MediaMetadataRetriever.OPTION_CLOSEST_SYNC); // 保存缩略图 thumbBitmaps[i] = ThumbnailUtils.extractThumbnail(bitmap, btWidth, btHeight, ThumbnailUtils.OPTIONS_RECYCLE_INPUT); // 刷新界面 postInvalidate(); } } catch (Exception e) { e.printStackTrace(); } finally { try { // 释放构造器资源 mediaRetriever.release(); } catch (Exception e2) { } } } /** buildThumbs Runnable 创建缩略图线程 */ private Runnable btRunn = new Runnable() { @Override public void run() { // 获取文件路径 File file = new File(videoUri); // 判断是否本地文件 if(file.exists()){ buildThumbsToLocal(); } } }; /** * 设置视频的总长度(内部处理 - MediaMetadataRetriever 获取) - 毫秒 * @param videoDuration */ private void setVideoDuration(int videoDuration){ this.videoDuration = videoDuration; } // ============== 内部控制代码 =============== /** 专门刷新View */ private Handler vhandler = new Handler(){ @Override public void handleMessage(Message msg) { switch(msg.what){ case 0: // 正常进行绘制触发 postInvalidate(); break; case 1: // 满一秒进行触发 displayTime = displayTime + 1000; // 累积时间// if(rtCallBack != null){// rtCallBack.preSecond(displayTime);// } break; } } }; /** 设备连接定时器 */ private Timer refTimer; /** 设备连接定时器任务栈 */ private TimerTask refTask; /** 整秒统计 */ private int iTime = 0; /** 刷新时间(毫秒) */ private int refTime = 500; // 250 /** 刷新频率 1000 / 刷新时间 */ private int refRate = 1000 / refTime; /** 对外获取时间 */ private long displayTime = 0l; /** * 设置定时器,刷新View * @param isOpen 是否打开 */ private void setTimer(boolean isOpen) { if (isOpen) { try { if (refTimer != null) { refTimer.cancel(); refTimer = null; } if (refTask != null) { refTask.cancel(); refTask = null; } } catch (Exception e) { } // 开启定时器 refTimer = new Timer(); // 每次重新new 防止被取消 // 重新生成定时器 防止出现TimerTask is scheduled already 所以同一个定时器任务只能被放置一次 refTask = new TimerTask() { @Override public void run() { // 累加播放时间 videoPlayProgress += refTime; // 如果大于总时间则进行重置 if(videoDuration != 0 && videoPlayProgress > videoDuration){ videoPlayProgress = videoDuration; // 进行通知最后一次 vhandler.sendEmptyMessage(0); // 并且关闭定时器 setTimer(false); return; } // -- vhandler.sendEmptyMessage(0); ++ iTime; if(iTime >= refRate){ vhandler.sendEmptyMessage(1); iTime = 0; // 满1秒 } } }; // xx秒后执行,每隔xx秒再执行一次 refTimer.schedule(refTask, 0, refTime); // 开启定时器 } else { try { if (refTimer != null) { refTimer.cancel(); refTimer = null; } if (refTask != null) { refTask.cancel(); refTask = null; } } catch (Exception e) { } vhandler.sendEmptyMessage(0); } } // ============== 对外公开方法 =============== /** * 销毁操作方法 */ public void destroy(){ clearThumbs(); } /** * 进行重置 */ public void reset(){ leftSX = 0f; // 重置到最左边 rightSX = 0f; // 重置到最右边 } /** * 是否允许裁剪(判断是否拖动) * @return */ public boolean isTrimVideo(){ if(leftSX != 0f || (rightSX != rightMarginX && rightSX != 0f && rightMarginX != 0f)){ return true; } return false; } /** * 获取开始时间(左边X轴转换时间) - 毫秒 * @return */ public float getStartTime(){ if(getViewWidthConvertTime() != -1){ return leftSX * xTime; } return -1f; } /** * 获取结束时间(右边X轴转换时间) - 毫秒 * @return */ public float getEndTime(){ if(getViewWidthConvertTime() != -1){ return rightSX * xTime; } return -1f; } /** * 设置视频进度条 * @param isCutMode 是否裁剪模式 * @param videoUri 视频路径 */ public void setVideoUri(boolean isCutMode, String videoUri) { setVideoUri(isCutMode, videoUri, -1f); } /** * 设置视频进度条 * @param isCutMode 是否裁剪模式 * @param videoUri 视频路径 * @param videoFrame 关键帧时间(毫秒) */ public void setVideoUri(boolean isCutMode, String videoUri, float videoFrame) { this.setCutMode(isCutMode); this.videoUri = videoUri; this.videoFrame = videoFrame; // -- // 生成缩略图 buildThumbs(); } /** * 设置当前播放进度 * @param curTime 当前的时间(毫秒) */ public void setProgress(int curTime){ this.videoPlayProgress = curTime; } /** * 是否绘制播放进度条 * @param isDrawProgressLine */ public void setProgressLine(boolean isDrawProgressLine){ this.isDrawProgressLine = isDrawProgressLine; // 判断是否需要开启定时器 setTimer(this.isDrawProgressLine || this.isDrawProgressBG); } /** * 是否绘制播放进度背景 * @param isDrawProgressBG */ public void setProgressBG(boolean isDrawProgressBG){ this.isDrawProgressBG = isDrawProgressBG; // 判断是否需要开启定时器 setTimer(this.isDrawProgressLine || this.isDrawProgressBG); } /** * 设置进度绘制相关功能(统一是否显示) * @param isDrawProgress */ public void setProgressDraw(boolean isDrawProgress){ this.isDrawProgressLine = isDrawProgress; this.isDrawProgressBG = isDrawProgress; // 判断是否需要开启定时器 setTimer(isDrawProgress); } /** * 设置裁剪模式 * @param isCutMode 是否裁剪 */ public void setCutMode(boolean isCutMode){ this.isCutMode = isCutMode; // 如果属于裁剪模式,则不绘制背景阴影 if (isCutMode){ this.isDrawProgressBG = false; } } /** * 设置裁剪模式 * @param isCutMode 是否裁剪 * @param isDrawProgressLine */ public void setCutMode(boolean isCutMode, boolean isDrawProgressLine){ this.setCutMode(isCutMode); // -- this.isDrawProgressLine = isDrawProgressLine; // 判断是否需要开启定时器 setTimer(isDrawProgressLine); }}
使用方法(项目内有):
代码下载
Android视频裁剪自定义View
1 0
- Android视频裁剪自定义View
- android 自定义裁剪View
- Android视频裁剪(含裁剪View)
- Android自定义裁剪图片的View
- Android 自定义View实现照片裁剪框与照片裁剪
- Android自定义 view之图片裁剪从设计到实现
- ANDROID 裁剪View
- Android 自定义View (四) 视频音量调控
- Android 自定义View (四) 视频音量调控
- Android 自定义View (四) 视频音量调控
- Android 自定义View (四) 视频音量调控
- Android 自定义View (四) 视频音量调控
- Android 自定义View (四) 视频音量调控
- Android 自定义View (四) 视频音量调控
- Android 自定义View (四) 视频音量调控
- Android 自定义View (四) 视频音量调控
- Android 自定义View (四) 视频音量调控
- Android 自定义View (四) 视频音量调控
- RxJava操作符——条件和布尔操作符(Conditional and Boolean Operators)
- 夕拾算法进阶篇:10)打印出栈序列&出栈序列是否合法(stack)
- 异常处理
- spark入门之四 任务的调度stages划分
- 少数派报告——树莓派搭建Tor匿名站点
- Android视频裁剪自定义View
- [Coursera机器学习]K-means Clustering and Principal Component Analysis WEEK8编程作业
- 二叉搜索树的后序遍历序列(二叉搜索树的应用)
- 268. Missing Number
- 一、Noip2003,数字游戏题解(环形DP)
- 低端笔记本安装Android studio 环境搭建
- 高效率工具整理
- 交换两个变量的值,不使用第三个变量的四种法方
- wamp安装以及域名的配置