android基于gpuimage和photoview的图片编辑(滤镜,饱和度,裁剪)

前言

此博客方便自己使用与他人交流,未经同意不允许他人转载
以前在项目中遇到图片处理的需求,滤镜、饱和度处理和裁剪功能,这里基于gpuimage实现图片滤镜和饱和度的功能,基于photoview图片放大,缩小,移动,实现裁剪功能。demo地址:ImageEdit
以下是两个开源项目的地址有兴趣的同学可以了解一下
android-gpuimage
PhotoView

图片位置处理

图片有长图和宽图所以gpuimage的位置大小并不是固定的,得依据图片大小动态调整,同时图片内存分辨率必须做相应处理
1.gpuimage位置处理:以宽度为360dp的屏幕为基准,做gpuimage的缩放

private void setImageVieSize(View view, int width, int height) {int fixSize = ViewSizeUtil.getCustomDimen(360f);ViewGroup.LayoutParams layoutParams = view.getLayoutParams();if (width == height) {width = height = fixSize;} else {if (width > height) {height = height * fixSize / width;width = fixSize;} else {width = width * fixSize / height;height = fixSize;}}layoutParams.height = height;layoutParams.width = width;}

2.图片分辨率处理:以2048×2048为标准,(imageloader里面默认能支持的最大分辨率,图片太大加载不出来就会提示超过此分辨率,这个分辨率的选择得看手机和内存使用情况,有可能出现oom,imageloader里面是以屏幕分辨率或者控件的大小对图片进行压缩,但是我们这里原则上是尽可能让图片清晰),如果以2的倍数来压,你会发现图片会变的很模糊,所以压缩后的分辨率尽可能保证接近2048×2048,并且对图片进行lrucache内存管理,优化总结,目前对图片分辨率的处理:

public static Bitmap getSuitBitmap(String filePath) {Bitmap bitmap;int degree = ImageUtil.readPictureDegree(uploadFilePath);//对图片做旋转处理if (uploadFilePath.startsWith("file://")) {uploadFilePath = uploadFilePath.substring(7);}bitmap = decodeBitmap(uploadFilePath);if (degree != 0) {bitmap = ImageUtil.rotateBitmap(bitmap, degree);}return bitmap;}
public static Bitmap decodeBitmap(String pathName) {Bitmap bitmap = null;try {BitmapFactory.Options options = getBitmapOptions();BitmapFactory.decodeFile(pathName, options);getAfterBitmap(options);String key = getKey(String.valueOf(options.outWidth), String.valueOf(options.outHeight), pathName);bitmap = getMemoryCacheBitmap(key);if (bitmap == null || bitmap.isRecycled()) {bitmap = BitmapFactory.decodeFile(pathName, options);putInMemoryCache(key, bitmap);}} catch (OutOfMemoryError e) {e.printStackTrace();}return bitmap;}public static void getAfterBitmap(BitmapFactory.Options options) {float widthRate = options.outWidth * 1.0f / 2048 * 1.0f;float heightRate = options.outHeight * 1.0f / 2048 * 1.0f;options.inSampleSize = 1;if (widthRate * heightRate > 1) {options.inSampleSize = (int) (widthRate * heightRate) + 1;}options.inJustDecodeBounds = false;}private static String getKey(String width, String height, String path) {return path + "_" + "width" + width + "_" + "height" + height;}

饱和度处理

效果图如下:

这个功能就是gpuimage的api调用而已,看完simple发现图片的处理大概就是以下几步:

GPUImageFilter filter = GPUImageFilterTools.createFilterForType(AppContext.context(), GPUImageFilterTools.FilterType.SATURATION);//.获取对应的GPUImageFilter
mFilterAdjuster = new GPUImageFilterTools.FilterAdjuster(filter);//.根据fliter获取FilterAdjustermGPUImageView.setImage(bitmap);//设置图片
mGPUImageView.setFilter(filter);//设置filtermFilterAdjuster.adjust(progress);//设置进度
mGPUImageView.requestRender();//刷新view

这个自定义的进度条不准备详细赘述,后面会专门花点时间写篇博客讲这些年写过的自定义控件(先挖个坑)

滤镜处理

效果图如下:

首先得根据原图生成各种滤镜效果,管理里面还有滤镜筛选和排序(这个排序功能第三方的很多,这里不做叙述),同时还要根据用户本地保存顺序和类型,处理内存问题。

首先增加滤镜数据,获取FilterType,GPUImageFilterTools类中定义了FilterType表示各种滤镜,选取了一部分滤镜,为了方便数据处理把enum,变成string

public static final String I_1977 = "48";//创新
public static final String I_AMARO = "49";//流年
public static final String I_BRANNAN = "50";//淡雅
public static final String I_EARLYBIRD = "51";//怡尚
public static final String I_HEFE = "52";//优格
public static final String I_HUDSON = "53";//胶片
public static final String I_INKWELL = "54";//黑白
public static final String I_LOMO = "55";//个性
public static final String I_LORDKELVIN = "56";//回忆
public static final String I_NASHVILLE = "57";//复古
public static final String I_RISE = "58";//森系
public static final String I_SIERRA = "59";//清新
public static final String I_SUTRO = "60";//摩登
public static final String I_TOASTER = "61";//绚丽
public static final String I_VALENCIA = "62";//优雅
public static final String I_WALDEN = "63";//日系
public static final String I_XPROII = "64";//新潮
public static final String DEFAULT = "65";//原图
private List<String> filters = new ArrayList();
private void initImageFilterData() {String imageFilters = SharedPreferenceHelper.getInstance().getImageFilters();//判断本地是否有滤镜if (imageFilters.isEmpty()) {StringBuffer stringBuffer = new StringBuffer();int start = Integer.valueOf(GPUImageFilterTools.FilterType.I_1977);int end = Integer.valueOf(GPUImageFilterTools.FilterType.I_HEFE);for (int i = start; i < end; i++) {filters.add(String.valueOf(i));stringBuffer.append(filters.get(i - start));stringBuffer.append(",");}if (stringBuffer.length() > 0) {stringBuffer.deleteCharAt(stringBuffer.length() - 1);}SharedPreferenceHelper.getInstance().putImageFilters(stringBuffer.toString());} else {String[] split = imageFilters.split(",");for (int i = 0; i < split.length; i++) {if (!split[i].isEmpty()) {filters.add(split[i]);}}}filters.add(0, GPUImageFilterTools.FilterType.DEFAULT);}//然后根据type获取filter,mList就是上文中的filters
public void addFilters() {this.gpuImageFilters = new ArrayList<>();for (int i = 0; i < mList.size(); i++) {gpuImageFilters.add(GPUImageFilterTools.createFilterForType(AppContext.context(), mList.get(i)));}}public static GPUImageFilter createFilterForType(final Context context, final String type) {switch (type) {case FilterType.DEFAULT:return new GPUImageFilter();case FilterType.CONTRAST:return new GPUImageContrastFilter(2.0f);case FilterType.GAMMA:return new GPUImageGammaFilter(2.0f);case FilterType.INVERT:return new GPUImageColorInvertFilter();case FilterType.PIXELATION:return new GPUImagePixelationFilter();case FilterType.HUE:return new GPUImageHueFilter(90.0f);    ............default:LogUtils.i("filter",type);throw new IllegalStateException("No filter of that type!");}

当选择滤镜时,同时更新原图的滤镜,这里绘制时会造成卡顿,所以得开个线程:

//处理滤镜选择和管理滤镜功能
onImageFilterSelectUpdateRecyclerListener.setOnImageFilterClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {int position = (int) v.getTag();if (position == onImageFilterSelectUpdateRecyclerListener.getCount() - 1) {ImageFilterManagerActivity.startActivityForResult(PostImageEditActivity.this, pImage.url, Constants.REQUEST_CODE_1001);} else {if (lastFilterPosition != position) {currentFilter = onImageFilterSelectUpdateRecyclerListener.getGpuImageFilters().get(position);setFilterBitmap(executorService, PostImageEditActivity.this, currentFilter, PostImageEditActivity.originalBitmap, mGPUImageView, position);}}}});//更新原图滤镜       public void setFilterBitmap(ExecutorService executorService, final BaseActivity baseActivity, GPUImageFilter filter, final Bitmap bitmap, final ImageView imageView, final int tempPosition) {baseActivity.showWaitDialog();GPUImage.getBitmapForFilter(executorService, bitmap, filter, new GPUImage.ResponseListener<Bitmap>() {@Overridepublic void response(final Bitmap item) {ImageEditApplication.getInstance().handler.post(new Runnable() {@Overridepublic void run() {baseActivity.hideWaitDialog();imageView.setImageBitmap(item);currentBitmap = item;lastFilterPosition = tempPosition;}});}});}//增加线程 GPUImage类中
public static void getBitmapForFilter(ExecutorService executorService, final Bitmap bitmap, final GPUImageFilter filter, final ResponseListener<Bitmap> listener) {executorService.execute(new Runnable() {@Overridepublic void run() {GPUImageRenderer renderer = new GPUImageRenderer(filter);renderer.setImageBitmap(bitmap, false);PixelBuffer buffer = new PixelBuffer(bitmap.getWidth(), bitmap.getHeight());buffer.setRenderer(renderer);renderer.setFilter(filter);if (listener != null) {listener.response(buffer.getBitmap());}filter.destroy();renderer.deleteImage();buffer.destroy();}});}
//recyclerView中内存处理if (position == getCount() - 1) {imageView.setTag(null);name = R.string.timeline_manage;imageView.setImageResource(R.drawable.icon_image_filter);holder.setBackgroundRes(R.id.item_filter_image_container, R.drawable.rectangle_image_filter_setting);imageLayoutParams.height = imageLayoutParams.width = ViewSizeUtil.getCustomDimen(29f);} else {imageLayoutParams.height = imageLayoutParams.width = ViewSizeUtil.getCustomDimen(94f);name = context.getResources().getIdentifier("text_filter_" + mList.get(position), "string", AppContext.getContext().getPackageName());holder.setBackgroundRes(R.id.item_filter_image_container, R.color.white);Bitmap bitmap = mMCache.get(mList.get(position) + path);//利用imageloader的内存管理imageView.setTag(mList.get(position) + path);if (bitmap != null && !bitmap.isRecycled()) {imageView.setImageBitmap(bitmap);} else {final GPUImageFilter filter = gpuImageFilters.get(position);ImageFilterHandler.setFilterBitmap(executorService, filter, mList.get(position) + path, this.bitmap, imageView);//获取滤镜后的图片,并进行内存管理}}
//   ImageFilterHandler类中
public static void setFilterBitmap(ExecutorService executorService, GPUImageFilter filter, final String key, Bitmap bitmap, final ImageView imageView) {GPUImage.getBitmapForFilter(executorService, bitmap,filter, new GPUImage.ResponseListener<Bitmap>() {@Overridepublic void response(final Bitmap item) {MemoryCache mMCache = ImageLoader.getInstance().getMemoryCache();mMCache.put(key,item);String tag = (String) imageView.getTag();if (tag.equals(key)) {AppContext.context().handler.post(new Runnable() {@Overridepublic void run() {imageView.setImageBitmap(item);}});}}});}

图片裁剪:

横图


1.横图初始时图片高和剪裁区域等高,可以左右滑动,不能上下滑动,
2.点击缩放按钮,图片宽和剪裁区域等宽,不能滑动,
3.双击能进行2次放大,超过裁剪区域能进行移动

长图
1.初始等宽,超过裁剪区域能移动
2.点击缩放按钮,图片高和剪裁区域等高,不能滑动
3.双击能进行2次放大,超过裁剪区域能进行移动

photoview已经把图片放大,缩小,移动的功能做的很强大了,我们只需要加个剪裁区域盖在上面,同时限制移动的区域,往下移动时图片顶部最低和裁剪区域顶部等高不得低于裁剪区域顶部,往上移动时图片底部和和裁剪区域底部登高不得高于裁剪区域底部,左右移动时规则一样(仿instagram),没办法photoview源码还得走起一波
1.得知道裁剪区域上下左右的位置
2.得知道photoview移动的方法
先解决第一个问题:除掉取消,还原,完成剩下的区域都是图片可以显示的区域,裁剪区域在剩下的空间居中:

 private int setCustomBounds() {//image_container是个相对布局,里面的view就是裁剪区域和imageview,dp_360以360dp屏幕为标准适配的
private int setCustomBounds() {int deltHeight = (image_container.getHeight() - dp_360) / 2;RectF rectF = new RectF();rectF.left = 0;rectF.right = dp_360;rectF.top = deltHeight;rectF.bottom = rectF.top + dp_360;mAttacher.setCustomBounds(rectF);return deltHeight;}//设置裁剪区域上边界和下边界的两条线private void setMask(int deltHeight) {View bottom = getView(R.id.clip_bounds_view_below);bottom.setBackgroundResource(R.color.black_overlay);View above = getView(R.id.clip_bounds_view_above);above.setBackgroundResource(R.color.black_overlay);above.getLayoutParams().height = bottom.getLayoutParams().height = deltHeight;}//photoview矩阵初始化时会调用setOnMatrixChangeListener,我们必须根据是长图还是宽图把图片进行缩放,达到我们之前说的初始状态,初始化完成之后这段逻辑就不需要走了,因此给imageview加个tag做标记,baseScaleRate记录初始化时的缩放倍率,方便还愿功能用到
imageView.setTag(true);
mAttacher.setOnMatrixChangeListener(new PhotoViewAttacher.OnMatrixChangedListener() {@Overridepublic void onMatrixChanged(RectF rect) {//Rectf为当前显示区域Object tag = imageView.getTag();if (tag != null) {boolean flag = (boolean) tag;baseRectF = new RectF(rect);if (bitmap.getHeight() >= bitmap.getWidth()) {if (flag) {float bitmapRate = bitmap.getHeight() * 1.0f / bitmap.getWidth() * 1.0f;float viewRate = image_container.getHeight() * 1.0f / image_container.getWidth() * 1.0f;//图片是否超过imageview的控件区域,因为imageview是充满剩余空间的,超过就需要缩放,未超过不需要if (bitmapRate > viewRate) {imageView.setTag(false);baseScaleRate = dp_360 * 1.0f / rect.width() * 1.0f;if (baseScaleRate > 3.0f) {mAttacher.setMaximumScale(baseScaleRate + 2);}mAttacher.setScale(baseScaleRate);} else {imageView.setTag(null);}} else {imageView.setTag(null);}float height = image_container.getHeight() * 1.0f > baseRectF.height() ? baseRectF.height() : image_container.getHeight() * 1.0f;
//                        mAttacher.setCustomMinScale(dp_360 * 1.0f / height);mAttacher.setMinimumScale(dp_360 * 1.0f / height);} else {if (flag) {imageView.setTag(false);baseScaleRate = dp_360 * 1.0f / rect.height() * 1.0f;if (baseScaleRate > 3.0f) {mAttacher.setMaximumScale(baseScaleRate + 2);}mAttacher.setScale(baseScaleRate);mAttacher.setMinimumScale(baseRectF.height() * 1.0f / dp_360 * 1.0f);//设置最小缩放比率} else {imageView.setTag(null);}}}}});   //缩放按钮逻辑就是设置缩放倍率而已
setOnClickListener(R.id.photo_full_view, new View.OnClickListener() {@Overridepublic void onClick(View v) {if (bitmap.getHeight() >= bitmap.getWidth()) {if (mAttacher.getScale() < baseScaleRate) {mAttacher.setScale(baseScaleRate);} else {float height = image_container.getHeight() * 1.0f > baseRectF.height() ? baseRectF.height() : image_container.getHeight() * 1.0f;mAttacher.setScale(dp_360 * 1.0f / height);}} else {if (mAttacher.getScale() < baseScaleRate) {mAttacher.setScale(baseScaleRate);} else {mAttacher.setScale(baseRectF.height() * 1.0f / dp_360 * 1.0f);}}}});
//ClipBoundsView宽高和屏幕等宽里面就画了4条线,边界的线放在外面还好处理一点,里面测量写死了,

限制photoview移动区域

//设置photoview矩阵移动边界
private int setCustomBounds() {int deltHeight = (image_container.getHeight() - dp_360) / 2;RectF rectF = new RectF();rectF.left = 0;rectF.right = dp_360;rectF.top = deltHeight;rectF.bottom = rectF.top + dp_360;mAttacher.setCustomBounds(rectF);//增加我们的限制条件方法return deltHeight;}
//接下来设置photoview移动范围,在PhotoViewAttacher类中有fling方法处理滑动逻辑的
public void fling(int viewWidth, int viewHeight, int velocityX,int velocityY) {final RectF rect = getDisplayRect();if (null == rect) {return;}final int startX = Math.round(-rect.left);final int minX, maxX, minY, maxY;if (viewWidth < rect.width()) {minX = 0;maxX = Math.round(rect.width() - viewWidth);} else {minX = maxX = startX;}final int startY = Math.round(-rect.top);if (customBounds != null) {//此处加上我们的限制条件,scroller移动向上为正,向下为负,minY就是向下移动的距离,maxy就是向上移动的距离,向下能滚到裁剪区域的顶部就是在原来的基础上向下滑动 -(int) customBounds.top ,customBounds == null的情况就是原来的代码,向上在原来的基础上加上 customBounds.top,这样fling移动的过程搞定(实在不好理解就打断点,看看图片移动的过程就理解了)if (customBounds.height() < rect.height()) {minY = -(int) customBounds.top;//maxY = (int) (Math.round(rect.height() - viewHeight) + customBounds.top);//} else {minY = maxY = startY;}} else {if (viewHeight < rect.height()) {minY = 0;maxY = Math.round(rect.height() - viewHeight);} else {minY = maxY = startY;}}mCurrentX = startX;mCurrentY = startY;if (DEBUG) {LogManager.getLogger().d(LOG_TAG,"fling. StartX:" + startX + " StartY:" + startY+ " MaxX:" + maxX + " MaxY:" + maxY);}// If we actually can move, fling the scrollerif (startX != maxX || startY != maxY) {mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0);}}
//还有一个处理的地方是双手缩放的时候,必须设置矩阵的边界,ctr+F12查找方法找到checkMatrixBounds()这个就是处理矩阵边界的方法,依据源码的逻辑(customBounds == null的情况下的处理)依葫芦画瓢增加相应的逻辑
private boolean checkMatrixBounds() {final ImageView imageView = getImageView();if (null == imageView) {return false;}final RectF rect = getDisplayRect(getDrawMatrix());if (null == rect) {return false;}final float height = rect.height(), width = rect.width();float deltaX = 0, deltaY = 0;final int viewHeight = getImageViewHeight(imageView);if (customBounds != null) {if (height <= customBounds.height()) {switch (mScaleType) {case FIT_START:deltaY = -rect.top;break;case FIT_END:deltaY = viewHeight - height - rect.top;break;default:deltaY = (viewHeight - height) / 2 - rect.top;break;}} else if (rect.top > customBounds.top) {deltaY = -rect.top + customBounds.top;} else if (rect.bottom < customBounds.bottom) {deltaY = customBounds.bottom - rect.bottom;}final int viewWidth = getImageViewWidth(imageView);if (width <= customBounds.width()) {switch (mScaleType) {case FIT_START:deltaX = -rect.left;break;case FIT_END:deltaX = viewWidth - width - rect.left;break;default:deltaX = (viewWidth - width) / 2 - rect.left;break;}mScrollEdge = EDGE_BOTH;} else if (rect.left > customBounds.left) {mScrollEdge = EDGE_LEFT;deltaX = -rect.left + customBounds.left;} else if (rect.right < customBounds.right) {deltaX = customBounds.right - rect.right;mScrollEdge = EDGE_RIGHT;} else {mScrollEdge = EDGE_NONE;}} else {if (height <= viewHeight) {switch (mScaleType) {case FIT_START:deltaY = -rect.top;break;case FIT_END:deltaY = viewHeight - height - rect.top;break;default:deltaY = (viewHeight - height) / 2 - rect.top;break;}} else if (rect.top > 0) {deltaY = -rect.top;} else if (rect.bottom < viewHeight) {deltaY = viewHeight - rect.bottom;}final int viewWidth = getImageViewWidth(imageView);if (width <= viewWidth) {switch (mScaleType) {case FIT_START:deltaX = -rect.left;break;case FIT_END:deltaX = viewWidth - width - rect.left;break;default:deltaX = (viewWidth - width) / 2 - rect.left;break;}mScrollEdge = EDGE_BOTH;} else if (rect.left > 0) {mScrollEdge = EDGE_LEFT;deltaX = -rect.left;} else if (rect.right < viewWidth) {deltaX = viewWidth - rect.right;mScrollEdge = EDGE_RIGHT;} else {mScrollEdge = EDGE_NONE;}}// Finally actually translate the matrixmSuppMatrix.postTranslate(deltaX, deltaY);return true;}

图片裁剪保存
//思路,bitmap剪切问题不大,调用Bitmap.createBitmap方法,需要知道起始点位置,宽,高,只要计算出图片没有缩放的情况下,x,y的偏移,同时加上裁剪区域的位置。矩阵的资料时间隔的太久,忘了当时找了哪些资料了这篇不错android matrix 最全方法详解与进阶

private Bitmap clip() {RectF mClipBorderRectF = getClipBorder();final Drawable drawable = imageView.getDrawable();final float[] matrixValues = new float[9];Matrix displayMatrix = mAttacher.getDrawMatrix();displayMatrix.getValues(matrixValues);final float scale = matrixValues[Matrix.MSCALE_X] * drawable.getIntrinsicWidth() / bitmap.getWidth();final float transX = matrixValues[Matrix.MTRANS_X];final float transY = matrixValues[Matrix.MTRANS_Y];final float cropX = (-transX + mClipBorderRectF.left) / scale;final float cropY = (-transY + mClipBorderRectF.top) / scale;final float cropWidth = mClipBorderRectF.width() / scale;final float cropHeight = mClipBorderRectF.height() / scale;return  Bitmap.createBitmap(bitmap, (int) cropX, (int) cropY, (int) cropWidth, (int) cropHeight, null, false);}private RectF getClipBorder() {//获取裁剪区域在image_container中的相对父控件位置RectF mClipBorderViewRectF = new RectF(ViewSizeUtil.getViewRectInParent(clip_bounds_view, image_container));//获取图片相对父控件的位置  RectF displayRect = mAttacher.getDisplayRect();if (bitmap.getHeight() >= bitmap.getWidth()) {//mAttacher.getScale() = baseScaleRate就是和裁剪区域等宽的情况if (mAttacher.getScale() < baseScaleRate) {//如果是缩小的状态,图片的区域小于裁剪区域的,就是类似长图2的情况mClipBorderViewRectF.left = displayRect.left;mClipBorderViewRectF.right = displayRect.right;}} else {//mAttacher.getScale() = baseScaleRate就是和裁剪区域等高的情况if (mAttacher.getScale() < baseScaleRate) {//类似宽图1的情况mClipBorderViewRectF.top = displayRect.top;mClipBorderViewRectF.bottom = displayRect.bottom;}}return mClipBorderViewRectF;}

总结

主要的难点差不多就是这些,最后附上demo地址:ImageEdit
此博客方便自己使用与他人交流,未经同意不允许他人转载

android基于gpuimage和photoview的图片编辑(滤镜,饱和度,裁剪)相关推荐

  1. 基于GPUImage的多滤镜rtmp直播推流

    之前做过开源videocore的推流改进:1)加入了美颜滤镜; 2) 加入了librtmp替换原来过于简单的rtmpclient: 后来听朋友说,在videocore上面进行opengl修改,加入新的 ...

  2. Android使用GPUImage实现滤镜效果精炼详解(一)

    一.前期基础知识详解 "滤镜通常用于相机镜头作为调色.添加效果之用.如UV镜.偏振镜.星光镜.各种色彩滤光片.滤镜也是绘图软件中用于制造特殊效果的工具统称,以Photoshop为例,它拥有风 ...

  3. Android基于IIS的APK下载(五)IIS的配置

    这里使用的IIS是win7_64的. 步骤一:打开IIS.控制面板->管理工具(如果没有,请把查看方式调成大图标)->Internet 信息服务(IIS)管理器. 步骤二:配置网站目录 步 ...

  4. android 添加子view,Android基于Window.ID_ANDROID_CONTENT给定id添加子View

    Android基于Window.ID_ANDROID_CONTENT给定id添加子View 这一技术特点在一些视频播放器中比较有用. 例如代码: package zhangphil.demo; imp ...

  5. android11beta支持什么手机,Android 11 Beta1发布,新增多种功能,网友:Android基于 Flyme...

    原标题:Android 11 Beta1发布,新增多种功能,网友:Android基于 Flyme 6.11日消息,谷歌于今日凌晨正式推送了 Android 11 Beta 1 版系统,不仅新增了可悬浮 ...

  6. android组件用法说明,Android第三方控件PhotoView使用方法详解

    Android第三方控件PhotoView使用方法详解 发布时间:2020-10-21 15:06:09 来源:脚本之家 阅读:74 作者:zhaihaohao1 PhotoView的简介: 这是一个 ...

  7. android网络转圈,android基于dialog加载时转圈圈很好的demo

    [实例简介] 这是一个android基于dialog加载时转圈圈很好的完整demo,很适合新手学习,希望对有需要的朋友能得到帮助 [实例截图] [核心代码] dialog_anim └── dialo ...

  8. android 自定义域名,Android基于Retrofit2改造的可设置多域名的网络加载框架

    Android基于Retrofit2改造的可设置多域名的网络加载框架 1.使用说明 添加仓库 ``` allprojects { repositories { google() jcenter() m ...

  9. ffmpeg实战教程(八)Android平台下AVfilter 实现水印,滤镜等特效功能

    ffmpeg实战教程(八)Android平台下AVfilter 实现水印,滤镜等特效功能 ffmpeg实战教程(七)Android CMake avi解码后SurfaceView显示 本篇我们在此基础 ...

最新文章

  1. dfs找不到网络路径 windows_Windows Server DFS本地共享文件夹访问
  2. 使用ado直接连接mysql_使用ADO直接连Mysql ,不经过ODBC
  3. 激活,数据存储,吐司
  4. 需求管理工具比较 Doors_Requistie Pro_RDM
  5. Codeforces Round #381 (Div. 1) A. Alyona and mex 构造
  6. 跑山么、后浪们?2.0T+237匹大马力后驱CT4山路试驾体验
  7. Linux命令之shutdown
  8. Tomcat服务器内存修改
  9. 【sketchup 2021】草图大师的高级工具使用3【复杂贴图制作实例(山体和球面贴图、全景天空绘制、吊顶添加光带)、图层(标记)工具使用、视图与样式工具的常规使用与高级使用说明】
  10. React中ref的三种获取方式
  11. 一切成功源于积累——20140219 混沌理论三原则
  12. 电脑自带的应用商店连接不到服务器,win10应用商店无法连接服务器最佳解决方法...
  13. 计算机空格键作用,电脑键盘上的空格有什么用 键盘上空格的作用说明
  14. 透明图片怎么发给别人_新手微商没生意咋办?微商怎么做如何推广?不放弃微信就是等死!...
  15. 视频播放器(AVPlayer)
  16. 案例21:Java农产品供求信息系统设计与实现开题报告
  17. 高分辨率卫星影像建筑物变化检测
  18. (二)K8s踩坑记录
  19. 微信小程序实战教程-闫涛-专题视频课程
  20. 搜狗输入法 exe 文件列表

热门文章

  1. linux压缩GIF图片命令
  2. 欧洲统一语言参考标准C1,浅述欧洲统一语言参考标准.doc
  3. matlab错误使用cd输入参数太多,错误使用函数,输入参数太多怎么解决
  4. BCD码与10进制转换
  5. 微信小程序导航:官方文档+精品教程+demo集合(5月31日更新)
  6. docker push失败
  7. 【随学随想】 自适应过滤法预测时间序列
  8. 师兄帮帮忙 UVa12412 一个简单的成绩查询问题
  9. CANoe学习第一周——理解CAN IG(CAN 交互式发生器)
  10. Windows系统显示器分屏