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

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

  • 网格布局
  • 水平布局

自定义View

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

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

  1. 编写自定义属性
  2. 在构造函数中获取自定义属性
  3. onMeasure中测量宽高,如有需要务必考虑支持padding属性和wrap_content,margin不用管,它是由父布局控制的。
  4. 重写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.right&&event.getY()>mDelBound.top&&event.getY()<mDelBound.bottom;

            if (touchable&&mDelClickL!=null) {//点击删除键
                mDelClickL.onDelClickL();
                return true;
            }
        }

        return super.onTouchEvent(event);
    }

自定义ViewGroup

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

  1. 编写自定义属性
  2. 在构造函数中获取自定义属性
  3. 在onMeasure中测量宽高,并测量子view宽高。
  4. 重写onLayout来对子View进行布局。
  5. 如有需要重写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 < childrenCount; i++) {
            ImageView childrenView = (ImageView) getChildAt(i);

            if (mAdapter != null) {
                mAdapter.onDisplayImage(getContext(), childrenView, mImgDataList.get(i));
            }
            int left = (mGridSize + mGap) * i + getPaddingLeft();
            int top =  getPaddingTop();
            int right = left + mGridSize;
            int bottom = top + mGridSize;
            childrenView.layout(left, top, right, bottom);
        }


        int left = (mGridSize + mGap) * childrenCount + getPaddingLeft();
        int top = getPaddingTop();
        int right = left + mGridSize;
        int bottom = top + mGridSize;
        mAddView.layout(left, top, right, bottom);//调整mAddView的位置

        // 初始化左右边界值
        mLeftBorder=getChildAt(0).getLeft();
        mRightBorder=getChildAt(childrenCount).getRight();

        if(mRightBorder-mLeftBorder>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 < mLeftBorder) {//判断有没有划出边界,如果划出便还原。
                    scrollTo(mLeftBorder,0);
                }else if (getScrollX() + getWidth() + 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

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

发布了30 篇原创文章 · 获赞 287 · 访问量 43万+

猜你喜欢

转载自blog.csdn.net/maplejaw_/article/details/105618904