Android多点触控实现图片缩放预览
在我的博客 Android手势识别基础介绍 中介绍了 MotionEvent 的一些知识,看过的朋友或者对手势识别有了解的都应该知道一般的触摸事件也就是单点触控该如何处理,所以我的这篇博客要介绍的就是关于如何实现多点触控。
我在博客中也介绍了多点触控的知识,但是并没有用实例实践,这里就用一个例子来说明。相信大家都经常在手机上浏览图片,大家的应用上也一定可以对图片进行放大或者缩小的处理,来让用户更好的浏览图片。这个例子就用到了多点触控,当然还会有对图片的大小处理,我现在就来实现这个 Demo。
1、设置图片大小与位置
因为我们要做的是对图片的放大缩小处理,所以首先当然是要将图片完好的呈现在屏幕上。如果直接使用系统的 ImageView 来获取图片资源,那么图片的大小就很有可能成为问题,这里我们用到 ViewTreeObserver 的 onGlobalLayoutListener 接口来实现对 ImageView 宽高的监听。关于 ViewTreeObserver 的介绍,有兴趣的朋友可以看看我的博客 Android–ViewTreeObserver介绍。
public class MyImageView extends AppCompatImageView implements OnGlobalLayoutListener { /* * 初始化时缩放的量 */ private float initScale; /* *双击放大达到的量 */ private float midScale; /* * 放大的最大量 */ private float maxScale; private Matrix mScaleMatrix; public MyImageView(Context context) { this(context, null); } public MyImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mScaleMatrix = new Matrix(); setScaleType(ScaleType.MATRIX); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); getViewTreeObserver().addOnGlobalLayoutListener(this); } @Override public void onGlobalLayout() { getViewTreeObserver().removeOnGlobalLayoutListener(this); //得到控件的宽和高 int width = getWidth(); int height = getHeight(); //得到图片,以及宽和高 Drawable d = getDrawable(); if (d == null) { return; } int dw = d.getIntrinsicWidth(); int dh = d.getIntrinsicHeight(); float scale = 1.0f; if (dw > width && dh < height) { scale = width * 1.0f / dw; } if (dh > height && dw < width) { scale = height * 1.0f / dh; } if ((dw > width && dh > height) || (dw < width && dh < height)) { scale = Math.min(width * 1.0f / dw, height * 1.0f / dh); } /* * 得到了初始化时缩放的比例 */ initScale = scale; midScale = scale * 2; maxScale = scale * 4; /* * 将图片移动至控件的中心 */ int dx = getWidth() / 2 - dw / 2; int dy = getHeight() / 2 - dh / 2; mScaleMatrix.postTranslate(dx, dy); mScaleMatrix.postScale(initScale, initScale, width / 2, height / 2); setImageMatrix(mScaleMatrix); }}
首先我们用 onAttachedToWindow() 来设置监听。
onAttachedToWindow() 是在 Activity Resume 的时候被调用的,也就是 Activity 对应的 window 被添加的时候,且每个 view 只会被调用一次,父 view 的调用在前,不论 view 的 visibility 状态是什么都会被调用,适合做些 view 特定的初始化操作。当 ImageView 被载入 window 的时候,我们的监听器就能被注册了。
还有个 onDetachedFromWindow() 方法,是在 Activity Destroy 的时候被调用的,也就是 Activity 对应的 window 被删除的时候,且每个 view 只会被调用一次,父view的调用在后,也不论 view 的 visibility 状态是什么都会被调用,适合做最后的清理操作。
因为我们是在 onGlobalLayout() 中获得 ImageView 和 图片初始化的值,这个方法只需要调用一次,所以我们可以不用 onDetachedFromWindow(),直接在 onGlobalLayout() 中移除监听器即可。
设置好 onGlobalLayoutListener,我们可以在 onGlobalLayout() 中获取 ImageView 的各种值来为图片设置我们想要的大小,在我的那篇博客中有宽高获取方法的详细介绍。
这里使用 getWidth() 来获取 ImageView 的宽,要说为什么就要提它与 getMeasureWidth() 的区别。
从这幅图可以看到 getMeasuredHeight() 表示的是view的实际大小,控件在屏幕之外有可能还有一部分,使用这个方法可能就不能让整个图像完整的显现在用户的眼前。
getHeight() 表示的是view在屏幕上显示的大小,它取决于屏幕。
后面我们用 getDrawable() 获得 ImageView 的图片,得到图片的宽高后,通过与 ImageView 的宽高的比较的情况给缩放比率赋值。这里的赋值方法是无论图片大小如何,都能在 ImageView 中完整的显现。这里图片宽高的比率是不变的,如何想要让图片的宽高差距大些,可以分别为宽高设置缩放比例,得到自己想要的图片效果。
我们的这个缩放的效果是双击后变成原来的两倍,再放大就是最初的四倍,所以要有 midScale 和 maxScale 两个变量。
我们得到图片的缩放比率,就要把它作用到 ImageVIew 上,这里就可以用 Matrix 这个类。我来简单的说明一下 Matrix,它是一个有九个元素的一维数组,里面分别存着矩阵的各个参数,我们这里只要用它的 Trans 和 Scale 两种参数就好,赋值的方法也给我们封装好了,我们把 ImageView 的图片看作一个矩阵,将值存入 Matrix 然后设置即可。
要注意要在 ImageView 中使用 Matrix,要在 xml 文件中为 ImageView 添加 scaleType 属性,值为 Matrix;也可以在构造方法中 setScaleType(ScaleType.MATRIX)。
我们如果想要将图片移到 ImageView 的中心,就要获得图片移动到中心 x,y 坐标分别要变化的值。在一开始,图片是在 ImageView 的最上面,如果要移动到中心,其实就是图片的中心移动到 ImageView 的中心。
由此我们可以很轻松地看出移动的值。
2、ScaleGestureDetector
ScaleGestureDetector 是专门用于多点触控控制缩放的手势检测器,用法与 GestureDetector 类似,都是通过 onTouchEvent() 关联相应的 MotionEvent 的。使用该类时,用户需要传入一个完整的连续不断地 motion 事件。
1、常用方法
1.
/* * 构造方法 */public ScaleGestureDetector(Context context, ScaleGestureDetector.OnScaleGestureListener listener)
2.
/* * 如果手势处于进行过程中,返回 true,否则返回 false。 * /public boolean isInProgress ()
3.
/* * 返回手势过程中,组成该手势的两个触点的当前距离。 * 返回值 以像素为单位的触点距离。 * 有 getCurrentSpanX () 与 getCurrentSpanY (),返回对应坐标轴上的距离。 * /public float getCurrentSpan ()
4.
/* * 返回事件被捕捉时的时间。 * 返回值 以毫秒为单位的事件时间。 * /public long getEventTime ()
5.
/* * 返回当前手势焦点的 X 坐标。 * 如果手势正在进行中,焦点位于组成手势的两个触点之间,多为中点。 * 如果手势正在结束,焦点为仍留在屏幕上的触点的位置。 * 若 isInProgress()返回 false,该方法的返回值未定义。 * 返回值 返回焦点的 X 坐标值,以像素为单位。 */public float getFocusX ()
6.
/* * 返回当前手势焦点的 Y 坐标。 * 同 getFocusX () * /public float getFocusY ()
7.
/* * 返回手势过程中,组成该手势的两个触点的前一次距离。 * 返回值 两点的前一次距离,以像素为单位。 * 有 getPreviousSpanX () 与 getPreviousSpanY (),返回对应坐标轴上的距离。 * 假设有a,b,c三个手指,某一次a,b组成缩放手势,两者的距离是300 * 随后一直是b,c组成缩放手势,当c抬起时,b,c的距离时100。 * 此时,a,b会组成缩放手势,该值返回的就是300,而不是b,c的100。 */public float getPreviousSpan ()
8.
/* * 返回从前一个伸缩事件至当前伸缩事件的伸缩比率。 * 该值定义为 (getCurrentSpan() / getPreviousSpan())。 * 返回值 当前伸缩比率。指本次事件中的缩放值,并不是相对于最开始的值。 * /public float getScaleFactor ()
9.
/* * 返回前一次接收到的伸缩事件距当前伸缩事件的时间差,以毫秒为单位。 * 返回值 从前一次伸缩事件起始的时间差,以毫秒为单位。public long getTimeDelta ()
2、内部类
OnScaleGestureListener 是 ScaleGestureDetector 中的回调接口,手势发生时接收通知的监听器。主要有三个方法:
- onScale:缩放时触发的方法。返回值代表本次缩放事件是否已被处理。如果已被处理,那么 detector 就会重置缩放事件;如果未被处理,detector 会继续进行计算,修改 getScaleFactor() 的返回值,直到被处理为止。因此,它常用在判断缩放值达到一定数值时,是否进行缩放。
- onScaleBegin:缩放开始执行的方法。该 detector 是否处理后继的缩放事件。返回false时,不会执行 onScale() 方法。
- onScaleEnd:缩放结束时执行的方法。
SimpleOnScaleGestureListener 是一个方便使用的类。 若仅想监听一部分尺寸伸缩事件,可继承该类。
3、实现缩放
我们让 ImageView 继承 OnScaleGestureListener,用它就可以来监听我们的缩放事件。但触摸事件 MotionEvent 是不会直接传入 ScaleGestureDetector 的,我们还要用 OnTouchListener 监听再将事件传入 ScaleGestureDetector。
public class MyImageView extends AppCompatImageView implements OnGlobalLayoutListener, OnScaleGestureListener, OnTouchListener { /** * 捕获用户多指触控时缩放的比例 */ private ScaleGestureDetector mScaleGestureDetector; public MyImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mScaleMatrix = new Matrix(); setScaleType(ScaleType.MATRIX); mScaleGestureDetector = new ScaleGestureDetector(context, this); setOnTouchListener(this); } public float getScale() { float[] values = new float[9]; mScaleMatrix.getValues(values); return values[Matrix.MSCALE_X]; } @Override public boolean onScale(ScaleGestureDetector detector) { float scale = getScale(); float scaleFactor = detector.getScaleFactor(); if (getDrawable() == null) return true; if ((scale < maxScale && scaleFactor > 1.0f) || (scale > initScale && scaleFactor < 1.0f)) { if (scale * scaleFactor > maxScale) { scaleFactor = maxScale / scale; } if (scale * scaleFactor < initScale) { scaleFactor = initScale / scale; } mScaleMatrix.postScale(scaleFactor, scaleFactor, getWidth() / 2, getHeight() / 2); setImageMatrix(mScaleMatrix); } return true; } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { } @Override public boolean onTouch(View v, MotionEvent event) { mScaleGestureDetector.onTouchEvent(event); return true; }}
在设置好几个监听器并将 Event 配置给我们的 ScaleGestureDetector 后,我们可以在 onScale() 中控制图片的缩放比率了。
前面小小的介绍了 Matrix,说了它是九元素的一维数组,我们这里图片的ScaleX 和 ScaleY 是一样的,所以图片相较于 initScale 的缩放的比率可以看作是 ScaleX。
用 getScaleFactor() 获得这次的缩放值,放大就是大于1.0f,缩小就是小于2.0f,因为我们为缩放设置了最小和最大值,所以我们要判断这次事件的缩放值不会让 Scale 大于或者小于我们设置的值。
最后我们像设置 initScale 一样为图片设置 Matrix。
因为要用真机测试,反应有些慢,但还可以看到我们实现了图片的缩放,
4、设置缩放中心点
我们按照上面的方法是实现了图片的缩放,但大家应该发现了,我们图片的缩放中心一直是 ImageView 的中点,这是因为我们在设置 Matrix 的 Scale 的中心是(getWidth() / 2,getHeight() / 2),这里我们就要用 ScaleGestureDetector 的 getFocusX() 和 getFocusY() 方法,这样缩放的中心点就变成我们要缩放的位置了。
mScaleMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
但要注意的是,因为我们之前缩放的中心是 ImageView 的中心,所以无论怎么缩放,最后仍还是在屏幕中央,宽高也是对应好 ImageView 的。但换了缩放中心点,就会变成如下图所示:
这样的效果很明显是不合格的,所以我们在变换焦点缩放的时候还要控制图片的边界和位置。
/* * 获得图片的宽高以及图片l, t, r, b四个坐标的值 */private RectF getMatrixRect() { Matrix matrix = mScaleMatrix; RectF rectF = new RectF(); Drawable d = getDrawable(); if (d != null) { rectF.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); matrix.mapRect(rectF); } return rectF;}/** * 在缩放的时候进行边界和位置控制 */private void controlBorderAndCenter() { RectF rectF = getMatrixRect(); float deltaX = 0; float deltaY = 0; int width = getWidth(); int height = getHeight(); if (rectF.width() >= width) { if (rectF.left > 0) { deltaX = -rectF.left; } if (rectF.right < width) { deltaX = width - rectF.right; } } if (rectF.height() >= height) { if (rectF.top > 0) { deltaY = -rectF.top; } if (rectF.bottom < height) { deltaY = height - rectF.bottom; } } if (rectF.width() < width) { deltaX = width / 2f - rectF.right + rectF.width() / 2f; } if (rectF.height() < height) { deltaY = height / 2f - rectF.bottom + rectF.height() / 2f; } mScaleMatrix.postTranslate(deltaX, deltaY);}
我们要让图片完好的显示在屏幕上,达到我们要的效果,就必须要知道图片四个方向的坐标,白边的出现就是因为移动的偏差,所以要想让白边消失,就要在放缩的时候让偏移的位置移回来。
getMatrixRect() 方法用来设置一个与图片大小位置相同的矩阵,以此来获得图片在缩放过程中变化的坐标。首先用 RectF 的 set() 方法构建好与图片相同大小的矩阵(获得 width,height),再用 Matrix 的 mapRect() 方法为我们的矩阵映射值,用途是将矩阵平移到对应坐标的位置,以此获得 left,top 这些值。
获得矩阵后,就要在出现白边的情况时移动图片。那就要获得距离屏幕边界的偏差值,也就是 deltaX 和 deltaY,分别是 x 轴 和 y 轴上的偏差。
如上图所示,图片一移到屏幕外边,就将它移动一个偏差值,这里的 left 是负数。当然一偏出一点就会立刻移回,眼睛是看不出的。
因为当矩形的宽高小于屏幕宽高时,出现白边是很正常的,所以我们只要考虑移动到中央即可。我们的图片是宽大于高,所以是图片的高小于屏幕的高。
上图是计算宽的偏差,高的偏差也是同样计算。
所以过程就是缩放中图片边界出现在屏幕中时就移动图片,而高度小于屏幕高度的时候就一直让中心移动,这样就能完整的呈现图片并让图片一直在中央。
只要在 onScale() 中初始化好图片位置下面调用写好的 controlBorderAndCenter() 方法就好了。
5、图片的自由移动
如果单单只能缩放,却不能在放大的时候自由移动图片,这样肯定是不合格的。我们要做的就是让图片在多点触控的时候也可以进行移动,不用担心在缩放的时候会影响移动,因为移动无论是多指还是单点都是单方向的,而缩放是多方向的触摸事件,再怎么移动手指它们也只会同时发生,不会有很大的影响。
public class MyImageView extends AppCompatImageView implements OnGlobalLayoutListener, OnScaleGestureListener, OnTouchListener { /** * 上次触摸时手指的数量 */ private int mLastPointerCount; private float mLastX; private float mLastY; /** * 移动的最小值 */ private int mTouchSlop; /** * 是否可以移动 */ private boolean isCanMove; public MyImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mScaleMatrix = new Matrix(); setScaleType(ScaleType.MATRIX); mScaleGestureDetector = new ScaleGestureDetector(context, this); setOnTouchListener(this); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); } @Override public boolean onTouch(View v, MotionEvent event) { mScaleGestureDetector.onTouchEvent(event); float x = 0; float y = 0; //拿到多点触控的数量 int pointCount = event.getPointerCount(); for (int i = 0; i < pointCount; i++) { x += event.getX(i); y += event.getY(i); } x /= pointCount; y /= pointCount; if (mLastPointerCount != pointCount) { isCanMove = false; mLastX = x; mLastY = y; mLastPointerCount = pointCount; } switch (event.getAction()) { case MotionEvent.ACTION_MOVE : float dx = x - mLastX; float dy = y - mLastY; if (Math.sqrt(dx * dx + dy * dy) > mTouchSlop) { isCanMove = true; } if (isCanMove) { RectF rectF = getMatrixRect(); if (getDrawable() != null) { //如果当前图片的宽高小于屏幕宽高,不用移动 if (rectF.width() < getWidth()) { dx = 0; } if (rectF.height() < getHeight()) { dy = 0; } mScaleMatrix.postTranslate(dx, dy); controlBorderAndCenter(); setImageMatrix(mScaleMatrix); } } mLastX = x; mLastY = y; break; case MotionEvent.ACTION_UP : case MotionEvent.ACTION_CANCEL : mLastPointerCount = 0; break; } return true; }}
因为这是普通的移动手指,所以我们在 onTouch() 方法中就可以捕捉进行操作。要想自由移动图片,最关键的就是找到移动的中心点,如果是单点那就是这根手指的坐标,而如果是多点,那就是几根手指的中心。用 MotionEvent 的 getX() 方法就可以很轻松的找到,我这里会出现很多 MotionEvent 的方法和知识,不了解的朋友可以看我的博客Android手势识别基础介绍。
就是通过与上次的中心点的坐标的比较来获取本次触摸事件移动的距离,而如果在过程中多点触控的数量减少了,那么就要重新计算找到中心点,否则距离肯定就算不对了。mTouchSlop 是设置的移动最小值,小于这个值就不能移动,通过 getScaledTouchSlop() 可以获取。
因为移动如果不加限制的话也会很容易将图片移除屏幕,但这并不是用户希望的,所以我们也要控制它的白边,因为在上面以经有 controlBorderAndCenter() 方法,所以我们只要在进行移动的地方调用就可以了。
6、双击放大和缩小
我们再为图片添加双击缩放的功能,这里我们就要用到 GestureDetector 这个类,用它就能实现我们想要的双击操作。
我的博客中也介绍了 GestureDetector 这个类的相关接口,我们要使用它的 SimpleOnGestureListener。
public class MyImageView extends AppCompatImageView implements OnGlobalLayoutListener, OnScaleGestureListener, OnTouchListener { private boolean isAutoScale = false; private GestureDetector mGestureDetector; public MyImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mScaleMatrix = new Matrix(); setScaleType(ScaleType.MATRIX); mScaleGestureDetector = new ScaleGestureDetector(context, this); setOnTouchListener(this); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDoubleTap(MotionEvent e) { if (isAutoScale) return true; float x = e.getX(); float y = e.getY(); if (getScale() < midScale) { postDelayed(new AutoScaleRunnable(midScale, x, y), 16); isAutoScale = true; } else { postDelayed(new AutoScaleRunnable(initScale, x, y), 16); isAutoScale = true; } return true; } }); } /** * 自动放大缩小 */ private class AutoScaleRunnable implements Runnable { private float mTargetScale; private float x; private float y; private final float BIGGER = 1.07f; private final float SMALLER = 0.93f; private float tempScale; public AutoScaleRunnable(float mTargetScale, float x, float y) { this.mTargetScale = mTargetScale; this.x = x; this.y = y; if (getScale() < mTargetScale) { tempScale = BIGGER; } if (getScale() > mTargetScale) { tempScale = SMALLER; } } @Override public void run() { mScaleMatrix.postScale(tempScale, tempScale, x, y); controlBorderAndCenter(); setImageMatrix(mScaleMatrix); if ((tempScale > 1.0f && getScale() < mTargetScale) || (tempScale < 1.0f && getScale() > mTargetScale)) { postDelayed(this, 16); } else { float scale = mTargetScale / getScale(); mScaleMatrix.postScale(scale, scale, x, y); controlBorderAndCenter(); setImageMatrix(mScaleMatrix); isAutoScale = false; } } }}
因为 GestureDetector 同样要用要传入 MotionEvent,如果不做处理,可能会与其它的触摸事件起冲突,所以要在 onTouch() 中进行判断:
if (mGestureDetector.onTouchEvent(event)) return true;
在这里我设定双击时图片若没有 midScale 大,则放大为 midScale,否则则缩小为 initScale。但我们不能直接在 onDoubleTap() 中将图片直接缩放,因为图片会在瞬间变化,这看上去十分不舒服,为了更好的效果,我们应该让图片有一个缩放的过程,这里用到了 Runnable 和 postDelayed(),postDelayed() 就是在设置的时间后执行 Runnable 对象类似于定时器的功能。
在 Runnable 里设置了缓慢缩放的比率,当前缩放比例小于目标比率就放大,大于则缩小。因为我设置的缩放比率累加到最后并不是整数,所以要另外判断将误差填平。
7、与ViewPager的结合
到这里我们的已经能够很好的实现图片预览了,不过我们平时所用的浏览器,文件管理器还有一种手指触控的功能,那就是切换图片。切换图片在有图片浏览功能的 APP 中是很常见的,我们这里就利用 ViewPager 来实现图片的切换。
public class MainActivity extends AppCompatActivity { private ViewPager mViewPager; private int[] mImgIds = new int[] { R.drawable.t1, R.drawable.t2, R.drawable.t3 }; private List mImgs = new ArrayList(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mViewPager = (ViewPager) findViewById(R.id.id_vp); mViewPager.setAdapter(new PagerAdapter() { @Override public Object instantiateItem(ViewGroup container, int position) { MyImageView imageView = new MyImageView(MainActivity.this); imageView.setImageResource(mImgIds[position]); container.addView(imageView); mImgs.add(imageView); return imageView; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView(mImgs.get(position)); } @Override public int getCount() { return mImgIds.length; } @Override public boolean isViewFromObject(View view, Object object) { return view == object; } }); }}
我们 xml 布局文件就是一个 ViewPager,关于 ViewPager 的使用基本上都是同一套流程的,在我的博客ViewPager的基础使用介绍介绍它的用法,我这里就不提了。
可以看到虽然实现了 ViewPager 切换图片,但是很明显我们不能在放大的时候移动图片了,这是因为 ViewPager 拦截了我们移动图片的事件。所以我们要想在放大后移动图片,就需要设置父控件不拦截 ImageView 的事件。
case MotionEvent.ACTION_DOWN : if ((rectF.width() > getWidth()) || rectF.height() > getHeight()) { if (getParent() instanceof ViewPager) getParent().requestDisallowInterceptTouchEvent(true); } break;case MotionEvent.ACTION_MOVE: if ((rectF.width() > getWidth()) || rectF.height() > getHeight()) { if (getParent() instanceof ViewPager) getParent().requestDisallowInterceptTouchEvent(true); } break;
在 DOWN 和 MOVE 手势下如果图片的宽高大于屏幕宽高就不允许父控件拦截,这样就可以移动图片啦。
还有要注意的是,RectF 的 width() 方法返回的是 float,getWidth() 返回的是 int,这样就很容易出现误差,通常我们可以在 int 型数据后加上0.01,或者用 Math.abs 算它们的差值。
我们这篇关于多点触控的例子就介绍到这里,例子参考的是慕课网上的视频打造个性的图片预览与多点触控,大家可以去看看。
结束语:本文仅用来学习记录,参考查阅。
更多相关文章
- Android中的Cursor到底是什么?如何理解Cursor的方法都在做什么事
- Android(安卓)分享功能之 微信 图片分享
- Android(安卓)Volley完全解析(四),带你从源码的角度理解Volley
- Android(安卓)中View的绘制流程(结合图解及伪代码说明)
- 增加 Andorid手機電池續航力 ,一定有效的省電心得
- Android(安卓)Google Map API使用的八个步骤
- Android(安卓)录制手机屏幕视频生成GIF图片实例详解
- 不同手机在豌豆夹上显示设备同名的解决方法 && Android(安卓)获
- 2016年末,Android岗位BAT等大厂面试题知识点小结(一)Android基础部