自定义View系列:仿微信QQ等图片选择展示控件

Tanya ·
更新时间:2024-09-20
· 649 次阅读

本篇主要讲解如何实现一个简易的选择上传图片时的展示控件,该自定义控件继承自ViewGroup,支持网格排列,以及横向排列。最终效果如下图:

网格布局
水平布局
自定义View

上图中每个ImageView的右上角都有一个删除按钮,我们可以通过组合View或者自定义View的方式去实现,这里选择自定义方式,自定义GridItemView继承自ImageView

我们知道自定义View一般流程为:

编写自定义属性 在构造函数中获取自定义属性 在onMeasure中测量宽高,如有需要务必考虑支持padding属性和wrap_content,margin不用管,它是由父布局控制的。 重写onDraw来进行绘制,以达到我们所需要的效果。

由于我们只是想在原图上画一个删除按钮,因此只需重写onDraw即可。

private void init(){ mPaint=new Paint(Paint.ANTI_ALIAS_FLAG); mDelBound=new Rect(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int foregroundWidth=30; int foregroundHeight=30; int delBoundPadding=5; //先画灰色背景 mPaint.setColor(0x88000000); mDelBound.set(getWidth()-getPaddingRight()-foregroundWidth-delBoundPadding*2 ,getPaddingTop() ,getWidth()-getPaddingRight() ,getPaddingTop()+foregroundHeight+delBoundPadding*2); canvas.drawRect(mDelBound,mPaint); //再画两条交叉线 mPaint.setColor(0xffffffff); canvas.drawLine(mDelBound.left+delBoundPadding ,mDelBound.top+delBoundPadding ,mDelBound.right-delBoundPadding ,mDelBound.bottom-delBoundPadding,mPaint); canvas.drawLine(mDelBound.left+delBoundPadding ,mDelBound.bottom-delBoundPadding ,mDelBound.right-delBoundPadding ,mDelBound.top+delBoundPadding,mPaint); }

上述代码实现了画灰色背景和两条交叉钱,此外,还可以根据实际情况,直接使用drawBitmap来画按钮。

接下来,我们给这个删除按钮加上点击事件,通过接口的形式对外提供按钮点击功能。

@Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP) { boolean touchable = event.getX() > mDelBound.left && event.getX() mDelBound.top&&event.getY()<mDelBound.bottom; if (touchable&&mDelClickL!=null) {//点击删除键 mDelClickL.onDelClickL(); return true; } } return super.onTouchEvent(event); } 自定义ViewGroup

流程一般如下,但由于实际需求不同,并不是每个步骤都需要重写。

编写自定义属性 在构造函数中获取自定义属性 在onMeasure中测量宽高,并测量子view宽高。 重写onLayout来对子View进行布局。 如有需要重写onDraw来进行添加效果,绝大多数并不需要。

由于我们需要实现两种排列方式,所以onMeasureonLayout的代码如下:

@Override protected void (int widthMeasureSpec, int heightMeasureSpec) {//测量 if(mShowStyle==STYLE_HORIZONTAL){ measureHorizontal(widthMeasureSpec,heightMeasureSpec); }else{ measureVertical(widthMeasureSpec,heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if(mShowStyle==STYLE_HORIZONTAL){ layoutHorizontal(changed,l,t,r,b); }else { layoutVertical(changed,l,t,r,b); } }

measure的计算比较简单,主要是计算一下高度。

private void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec){ int width = MeasureSpec.getSize(widthMeasureSpec); int height; int totalWidth = width - getPaddingLeft() - getPaddingRight(); mGridSize = (totalWidth - mGap * (mColumnCount - 1)) / mColumnCount; //算出每个条目的大小,以宽度为标准。 height = mGridSize + getPaddingTop() + getPaddingBottom();//计算出高度 setMeasuredDimension(width, height); } private void measureVertical(int widthMeasureSpec, int heightMeasureSpec){ int width = MeasureSpec.getSize(widthMeasureSpec); int height; int totalWidth = width - getPaddingLeft() - getPaddingRight(); int totalCount=mImgDataList.size()+1;//把mAddView也给算进去 mGridSize = (totalWidth - mGap * (mColumnCount - 1)) / mColumnCount; //算出每个条目的大小,以宽度为标准。 int mRowCount= (int) Math.ceil((totalCount*1.0)/mColumnCount);//算出行数 height = mGridSize * mRowCount + mGap * (mRowCount - 1) + getPaddingTop() + getPaddingBottom();//计算出高度 setMeasuredDimension(width, height); }

layout中,横向布局当图片显示超过控件宽度时,我们希望能自动移动到最右边,代码如下,主要是通过scrollTo方法实现的,当mRightBorder-mLeftBorder>getWidth()时,即可认为图片显示大于控件宽度。

//水平布局 private void layoutHorizontal(boolean changed, int l, int t, int r, int b) { int childrenCount = mImgDataList.size(); for (int i = 0; i getWidth()){ scrollTo(mRightBorder - getWidth(),0); }else{ scrollTo(mLeftBorder,0); } }

网格布局的代码如下:

private void layoutVertical(boolean changed, int l, int t, int r, int b) { int childrenCount = mImgDataList.size(); for (int i = 0; i < childrenCount; i++) { ImageView childrenView = (ImageView) getChildAt(i); if (mAdapter != null) { mAdapter.onDisplayImage(getContext(), childrenView, mImgDataList.get(i)); } int rowNum = i / mColumnCount; int columnNum = i % mColumnCount; int left = (mGridSize + mGap) * columnNum + getPaddingLeft(); int top = (mGridSize + mGap) * rowNum + getPaddingTop(); int right = left + mGridSize; int bottom = top + mGridSize; childrenView.layout(left, top, right, bottom); } int rowNum = (childrenCount) / mColumnCount; int columnNum = (childrenCount) % mColumnCount; int left = (mGridSize + mGap) * columnNum + getPaddingLeft(); int top = (mGridSize + mGap) * rowNum + getPaddingTop(); int right = left + mGridSize; int bottom = top + mGridSize; mAddView.layout(left, top, right, bottom);//调整mAddView的位置 } 自定义滑动事件

当图片显示超过ViewGroup的宽度时,为了使交互体验更友好,需要加入滑动功能。在实现之前,我们务必理清楚ViewGroup中dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent中的作用以及事件传递机制。

当检测到水平布局进行了水平滑动时,应当拦截事件。

@Override public boolean onInterceptTouchEvent(MotionEvent event) { float x = event.getX(); switch(event.getAction()) { case MotionEvent.ACTION_DOWN: if(mScroller != null){ if(!mScroller.isFinished()){ mScroller.abortAnimation(); } } mLastX = x; //记住开始落下的屏幕点 break; case MotionEvent.ACTION_MOVE: int detaX = (int) (x-mLastX); if(Math.abs(detaX)>mTouchSlop&&mShowStyle==STYLE_HORIZONTAL){ return true; } break; } return super.onInterceptTouchEvent(event); }

重写onTouchEvent。在MotionEvent.ACTION_MOVE中通过scrollBy来实现滑动效果,在MotionEvent.ACTION_UP中使用Scroller加入惯性滑动效果以增强交互体验。

private float mLastX;//记录上次滑动的位置 @Override public boolean onTouchEvent(MotionEvent event) { boolean isTouch=false; acquireVelocityTracker(event); //触摸点 float x = event.getX(); switch(event.getAction()){ case MotionEvent.ACTION_DOWN: if(mScroller != null){ if(!mScroller.isFinished()){ mScroller.abortAnimation(); } } mLastX = x ; isTouch=false; break ; case MotionEvent.ACTION_MOVE: int detaX = (int)(mLastX-x); //每次滑动屏幕,屏幕应该移动的距离 if (getScrollX() + detaX mRightBorder) { if(mRightBorder-mLeftBorder>getWidth()){ scrollTo(mRightBorder - getWidth(),0); }else{ scrollTo(mLeftBorder,0); } }else{ scrollBy(detaX, 0); } mLastX = x ; isTouch=true; break ; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: isTouch=false; mVelocityTracker.computeCurrentVelocity(1000,maxFlingSpeed); int speed= (int) mVelocityTracker.getXVelocity(); if(Math.abs(speed)>minFlingSpeed){ mScroller.fling(getScrollX(), 0, -speed, 0, mLeftBorder, mRightBorder-getWidth(), 0, 0); invalidate(); } releaseVelocityTracker(); break; } return isTouch; } private void acquireVelocityTracker(MotionEvent event) { if(null == mVelocityTracker) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); } private void releaseVelocityTracker() { if(null != mVelocityTracker) { mVelocityTracker.clear(); mVelocityTracker.recycle(); mVelocityTracker = null; } }

最后,当我们给这个ViewGroup设置图片数据时,刷新布局代码如下:

private void refreshDataSet() {//根据数据,调整ImageView的数量 int oldViewCount = getChildCount()-1;//上次的item个数,由于多了个mAddView,因此要减去1 int newViewCount = mImgDataList.size();//这次的item个数 if (oldViewCount > newViewCount) { removeViews(newViewCount, oldViewCount - newViewCount); } else if (oldViewCount < newViewCount) { for (int i = oldViewCount; i < newViewCount; i++) { ImageView iv = getImageView(i); addView(iv, i,generateDefaultLayoutParams()); } } requestLayout(); } 最后

完整代码见:https://github.com/maplejaw/GridImageView

使用时直接拷贝以下三个文件到工程中即可:


作者:maplejaw_



view 选择 展示 图片 自定义view

需要 登录 后方可回复, 如果你还没有账号请 注册新账号
相关文章