Android——自定义镂空遮盖控件

Posted 安卓笨笨鸟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android——自定义镂空遮盖控件相关的知识,希望对你有一定的参考价值。

刚学完ViewDragHelper和PorterDuffXferMode的我,突然想做一个这样效果的自定义控件:点击ListView的列表项,通过ViewDragHelper用动画方式上下各弹出一个控件遮盖住ListView,这两个控件在遮盖listView的过程中有一部分是镂空的。先上效果图:
这里写图片描述

首先是进行页面的布局,让自定义控件PlayLayout继承自Franlayout,在最底层放的就是listView所在的子FramLayout(Id:midContent),然后依次在上面加上下两个看起来被分割的FrameLayout(这里直接写了自定义控件SplitLayout继承Framlayout,id分别为:topSplit,bottomSplit),最后还要放上旋转圆的部分(自定义控件ControlPanel,)

大致布局像这样:

<PlayLayout extends frameLayout  id:palyLayout>
    <FrameLayout id:midContent>//显示ListView的底层部分
    </FrameLayout>

    <SplitLayout  extends FrameLayout id:topSplit>//顶部的分割区
    </SplitLayout>

     <SplitLayout  extends FrameLayout id:bootomSplit>//底部分割
    </SplitLayout>

    <ControlPanel extends FrameLayout id:controlPanel>//旋转圆部分
    </ControlPanel>
</PlayLayout>

首先分析怎么设置上下SplitLayout弹出的工作:
1、在PlayLayout中的onFinishInflate()中获得各个子布局

   midContent = (ViewGroup) getChildAt(0);
   topSplit = (SplitLayout) getChildAt(1);
   bottomSplit = (SplitLayout) getChildAt(2);
   controlPanel = (ControlPanel) getChildAt(3);
   //SplitLayout.Position是一个enum 标志该SplitLayout是上半部分还是下半部分,默认下半部分
   topSplit.setPosition(SplitLayout.Position.TOP);

2、根据可以设置的splitScale即上下splitLayout高度比(默认0.5,上下各一半),在PlayLayout控件的onMeaure中设置他们的高度

 int count = getChildCount();
        for (int i = 0; i < count; i++) {
        //测量子控件
            measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
        }
        int w = MeasureSpec.getSize(widthMeasureSpec);
        int h = MeasureSpec.getSize(heightMeasureSpec);
        //根据分割比例计算上下分割控件各自的高度
        int topH = (int) (h * 1.0f * splitScale + 0.5f);
        int bottomH = h - topH;
        ViewGroup.LayoutParams params1 = topSplit.getLayoutParams();
        ViewGroup.LayoutParams params2 = bottomSplit.getLayoutParams();
       ViewGroup.LayoutParams params3 = controlPanel.getLayoutParams();
        params1.height = topH;
        params1.width = getMeasuredWidth();
        params2.height = bottomH;
        params2.width = getMeasuredWidth();
        params3.height = (int) (radias*2);
        params3.width = (int) (radias*2) ;
        //设置各自的高度
        topSplit.setLayoutParams(params1);//上半部分割控件
        bottomSplit.setLayoutParams(params2);//下半部分
        controlPanel.setLayoutParams(params3);//旋转圆盘

3.重写PlayLayout的onLayout方法,进行子控件的布局

 int h = getMeasuredHeight();
 int topH = (int) (h * 1.0f * splitScale + 0.5f);
 int bottomH = h - topH;
 //放置listview的底层的midContent正常的layout
 midContent.layout(left, top, right, bottom);
 //topSplit一开始我们让其在最上面隐藏,点击listItem后才出来
 topSplit.layout(left, -topH, right, 0);
 //同样,bottomSplit最开始让其在父控件的底部隐藏
 bottomSplit.layout(left, h, right, h + bottomH);
 controlPanel.layout((int)(getMeasuredWidth()*1.0/2-radias),
                (int)(topH-radias),
                (int)(getMeasuredWidth()*1.0/2+radias),
                (int) (topH+ radias));

4.通过ViewDragHelper来处理滚动,接下来是ViewDragHelper的一般过程

先初始化

 public enum Status {//一共三个状态,打开,关闭,正在拖动
        OPEN, DRAGING, CLOSE
    }
private Status status = Status.CLOSE;
private ViewDragHelper mDragHelp;
//初始化
 private void initView() {
        mDragHelp = ViewDragHelper.create(this, mDragCallbak);
        status = Status.CLOSE;//初始化状态,splitLayout和controlPanel处于关闭状态
    }

当size发生变化时,重置一些参数

//这里是设置底部的splitLayout可以拖拽
private int mTop;//当前拖拽的位置
private int mStartTop;//初始Y方向top位置
private int mEndTop;//打开后,拖拽到的位置
private int mRange;//可以拖动的范围

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        int hh = getMeasuredHeight();
        int topH = (int) (hh * 1.0f * splitScale + 0.5f);
        mStartTop = hh;//最开始是在底部
        mEndTop = topH;//能够最终拖拽到父控件顶部splitLayout高度大小的位置
        mTop = hh;//初始化当前位置
        mRange = mStartTop - mEndTop;//计算拖拽范围
    }

拦截时间,以及处理事件

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (status == Status.CLOSE) return false;
        return mDragHelp.shouldInterceptTouchEvent(ev);
    }

@Override
public boolean onTouchEvent(MotionEvent event) {
        mDragHelp.processTouchEvent(event);
        return true;
    }

初始化ViewDragHelper.Callback

 private ViewDragHelper.Callback mDragCallbak = new ViewDragHelper.Callback() {

        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return child == bottomSplit;//选择能够拖动的控件
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
          //设置当前的拖拽位置,要在拖动范围之内
            if (top > mStartTop)
                top = mStartTop;
            if (top < mEndTop)
                top = mEndTop;
            return top;
        }

        //当拖动的控件位置发生变化时,调用该函数,在函数里更新当前的top位置mTop
        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);

            mTop += dy;
            mTop = Math.max(Math.min(mTop, mStartTop), mEndTop);
            dispatchScrollEvent();
        }

        //设置拖动范围
        @Override
        public int getViewVerticalDragRange(View child) {
            return mRange;
        }

        //当拖拽放开时
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            if (mTop < mEndTop + mRange / 2) {
                open();
            } else {
                close();
            }

            ViewCompat.postInvalidateOnAnimation(PlayLayout.this);                                                     
        }
    };

还需要重写onComputeScroll(和Scroll.startScrolll方法一样)

 @Override
    public void computeScroll() {
        super.computeScroll();
        if (mDragHelp.continueSettling(true))
            ViewCompat.postInvalidateOnAnimation(this);
    }

ViewDragHelper的惯性操作都放在open和close

public void open() {
        mDragHelp.smoothSlideViewTo(bottomSplit, 0, mEndTop);
        ViewCompat.postInvalidateOnAnimation(this);
    }

    private void close() {
        mDragHelp.smoothSlideViewTo(bottomSplit, 0, mStartTop);
        ViewCompat.postInvalidateOnAnimation(this);
    }

其中在ViewDragHelper.Callback的onViewPositionChanged中每次调用我们都计算了当前top,并分发了事件:

private Status updateStatus() {
        if (mTop <= mEndTop + 5) {
            status = Status.OPEN;
            System.out.println("status->>" + "open");
        } else if (mTop >= mStartTop - 5) {
            status = Status.CLOSE;
            System.out.println("status->>" + "close");

        } else {
            status = Status.DRAGING;
        }
        return status;
    }
 private void dispatchScrollEvent() {
        updateStatus();//根据当前mTop计算状态
        //计算拖拽完成的程度
        float percent = (mStartTop - mTop) * 1.0f / mRange;
        //根据百分比,移动呈现顶部splitLayout
        ViewHelper.setTranslationY(topSplit, percent * 1.0f * mEndTop + 0.5f);
        ViewHelper.setScaleY(controlPanel, percent);
        ViewHelper.setScaleX(controlPanel, percent);
        ViewHelper.setRotation(controlPanel, (1 - percent) *360);
    }

5.通过PorterDuffXferMode遮罩层设置镂空的SplitLayout
这里是在SplitLayout控件中
首先初始化

  private PorterDuffXfermode mMode;
  private Paint mPaint;
  private Bitmap mBg;//控件背景
 private void initView() {
        mPaint = new Paint();
        mPaint.setAlpha(0);//镂空遮罩层画笔一定要透明
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    }

然后重写onDraw时这样处理

 int w = getMeasuredWidth();
        int h = getMeasuredHeight();
        Bitmap bm = null;
        if (mBg != null) {
            int bitW = mBg.getWidth();
            int bitH = mBg.getHeight();
            //因为该控件只显示父控件一定比例的高度,所以背景比例是按父控件高度计算的
            float scaleH = h * 1.0f / splitScale / bitH;//h*(1/splitScale)得到父控件高度
            float scaleW = w * 1.0f / bitW;
            Matrix matrix = new Matrix();
            matrix.setScale(scaleW, scaleH);
            bm = Bitmap.createBitmap(mBg, 0, 0, bitW, bitH, matrix, true);
        }
        int startDegree = mPosition == Position.TOP ? 180 : 0;
        //利用同样大小的bitMap做镂空层
        Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        Canvas canvas1 = new Canvas(bitmap);

        //根据Position决定绘制已经缩放图像的上半还是下半,在下半则起点为父控件高度减去自己的高度
        int t = mPosition == Position.TOP ? 0 : (int) (h * 1.0f / splitScale - h);
        if (mBg == null)
            canvas1.drawColor(Color.argb(200, 128, 128, 128));
        else
            canvas1.drawBitmap(bm,
                    new Rect(0, t, w, t + h),
                    new Rect(0, 0, w, h),
                    null);

        //设置镂空圆的位置
        float top = h - radias;
        if (mPosition == Position.BOTTOM)
            top = -radias;
        float bottom = top + 2 * radias;
        RectF rectF = new RectF(w / 2 - radias, top, w / 2 + radias, bottom);
        //上面再bitmap上已经画了背景图片,再通过带xferMode属性的透明画笔去镂空背景图片,让其显示控件下面一层的控件视图
        canvas1.drawArc(rectF, startDegree, 180, true, mPaint);//注意mPaint的特殊设置,alpha=0,xmode(Mode.SRC_In);
        //将镂空层画到背景上
        canvas.drawBitmap(bitmap, 0, 0, null);

旋转圆控件的onDraw处理和SplitLayout基本一样,除了要镂空

 int bitW = mBg.getWidth();
            int bitH = mBg.getHeight();
            System.out.println("sout->>bitH=" + bitH + " bitW=" + bitW);
            //计算缩放比例
            float scaleH = h * 1.0f / bitH;
            float scaleW = w * 1.0f / bitW;
            Matrix matrix = new Matrix();
            matrix.setScale(scaleW, scaleH);
            Bitmap bitmap=Bitmap.createBitmap(mBg,0,0,bitW,bitH,matrix,true);
            mShader=new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);//通过BitmapShader很容易自定义各种什么圆角,圆形背景控件
            mPaint.setShader(mShader);
            canvas.drawCircle(w/2,h/2,radias,mPaint);

注意:要想实现镂空还得,必须在layout:xml布局文件里面指定background如下:

 <com.fan.skymusic.SplitLayout
            android:background="#00ffffff"
            android:id="@+id/topSplit"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
        </com.fan.skymusic.SplitLayout>

这里也有个问题搞不懂?
当我这这样设置时镂空的地方成了黑色,也不显示下层控件

 <com.fan.skymusic.SplitLayout
            android:background="#ffffff"
            android:alpha="0"
            android:id="@+id/topSplit"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
        </com.fan.skymusic.SplitLayout>

(android:background=”#00ffffff”)和(android:background=”#ffffff” android:alpha=”0”) 不应该是一样的吗

demo源代码地址(github新手,文件在app module 里面):
https://github.com/yifantao/SkyMusic

以上是关于Android——自定义镂空遮盖控件的主要内容,如果未能解决你的问题,请参考以下文章

Android中toolbar遮盖住其他控件该怎么解决

Android 软键盘的全面解析,让你不再怕控件被遮盖

Android - 如何将自定义对象传递给片段

从android中的片段更改自定义ActionBar标题

片段中ListView的android自定义适配器

Android:将形状遮盖到视图上的最简单方法是啥? [复制]