【安卓/Android】自定义控件之弧形布局及clip系列方法抗锯齿

开头必水,说是做弧形布局,不如说是Draw绘制这块踩坑,因为会对绘制结果进行裁剪(clipPath),而Path则是绘制贝塞尔曲线的结果。裁剪的通病用过的基本都知道,那就是抗锯齿,非常恶心,各种抗锯齿的办法基本GG。所以...我不打算用clip系列的办法(滑稽.jpg),而是Xfermode。Xfermode用起来问题也不大(参考链接),链接是别人的,基本知道是个啥就行了,没必要全部看完。

弧形布局大概长这样:

弧形布局 

基本就是一个 FrameLayout 里边重写dispatchDraw()方法,通过绘制贝塞尔曲线 + Xfermode实现。具体方法是先在一个空白画布上绘制好一个弧形,通过Xfermode实现最终效果。

 基本的初始化

    private Paint mPaint;
    private PaintFlagsDrawFilter mDrawFilter;
    private PorterDuffXfermode mXfermode;
    private Path mPath;

    private void init() {
        mPaint = new Paint();
        //绘制贝塞尔曲线
        mPath = new Path();
        //启用硬件加速,否则会出现一些异常(比如黑边,设计器和模拟器可能依旧会存在黑边)
        setLayerType(View.LAYER_TYPE_HARDWARE, mPaint);

        //消除画布的抗锯齿
        mDrawFilter = new PaintFlagsDrawFilter(
                0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG
        );

        /* 绘制时根据位置消除上层/下层部分的图层
         * 因为处于bottom时,弧形会遮住整个布局,如果采用上层(DST_OUT)则会消除弧形和布局交集的部分,
         * 所以bottom时采用下层(DST_IN) */
        mXfermode = new PorterDuffXfermode(
                arcDirection == 0 ? PorterDuff.Mode.DST_OUT : PorterDuff.Mode.DST_IN
        );
    }

绘制弧形路径

参考链接

    /**
     * 绘制弧形路径
     */
    private void drawArcPath() {
        float x1 = (getWidth() / 2F);
        mPath.lineTo(0, getDirectionHeight());
        mPath.quadTo(x1, getArcYHeight(), getWidth(), getDirectionHeight());
        mPath.lineTo(getWidth(), 0);
        mPath.close();
    }

如果参考链接依旧不理解,可以看看这个示意图,可能比较难理解的就是quadTo()方法,通俗点讲就是前两个参数主要确定弧形的顶点坐标(凸起的中间点坐标),后两个参数则是宽高(其中getDirectionHeight()接下来会讲到)。

弧形示意图

因为我们有上弧和下弧,上下通过高度决定,所以获取高度这块我们独立写个方法getDirectionHeight()以及控制弧形的朝向(内/外)getArcYHeight()。至于为什么通过高度决定上下弧,看看接下的示意图就知道了。

示意图

    //控制朝向。0:朝内,1:朝外
    private int arcOrientation;
    //控制上下弧。0:上弧,1:下弧
    private int arcDirection;

控制朝向 

    /**
     * 弧形Y轴高度(贝塞尔曲线凸/凹起部分的Y轴)
     * @return  根据绘制位置返回Y轴高度
     */
    private int getArcYHeight() {
        //决定朝内还是朝外
        int defHeight = (arcOrientation == 1 ? -arcHeight : arcHeight * 2);
        switch ( arcDirection ) {
            case 0: //顶部绘制弧形
                return defHeight;
            case 1: //底部绘制弧形
                return getHeight() - defHeight;
        }
        return 0;
    }

上下弧高度

    /**
     * 绘制弧形的高度
     * @return  根据绘制位置返回绘制高度
     */
    private int getDirectionHeight() {
        boolean isIn = arcOrientation == 0;
        int defHeight = arcHeight + arcOffsetY;
        switch ( arcDirection ) {
            case 0: //顶部绘制弧形
                return isIn ? 0 : defHeight;
            case 1: //底部绘制弧形
                return isIn ? getHeight() : getHeight() - defHeight;
        }
        return 0;
    }

有了上边的准备工作,接下来就可以绘制弧形了。

绘制弧形

    /**
     * 绘制弧形部分的图片。
     * 因为用的一个paint,所以需要在设置{@link Paint#setXfermode(Xfermode)}之前获取弧形图片
     */
    private Bitmap getDrawArcBitmp() {
        //建立一个空白图片,在里边绘制弧形路径
        Bitmap arcBmp = Bitmap.createBitmap(
                getWidth(), getHeight(), Bitmap.Config.ARGB_8888
        );

        //建立画布
        Canvas canvas = new Canvas( arcBmp );
        //消除画布的抗锯齿
        canvas.setDrawFilter( mDrawFilter );
        //绘制弧形路径
        drawArcPath();
        //启用抗锯齿
        mPaint.setAntiAlias( true );
        //绘制路径
        canvas.drawPath(mPath, mPaint);
        return arcBmp;
    }

最后,通过重写onDraw在布局中绘制弧形

重写onDraw方法

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        //获取绘制弧形部分的图片
        Bitmap arcBmp = getDrawArcBitmp();
        //消除画布的抗锯齿
        canvas.setDrawFilter( mDrawFilter );
        //绘制时消除绘制的弧形画布 (或者注释掉看效果就明白了)
        mPaint.setXfermode( mXfermode );
        //绘制最终的结果
        canvas.drawBitmap(arcBmp, 0, 0, mPaint);
        mPaint.setXfermode( null );
    }

完整代码在我的码云仓库里边,XML参数中只有ArcLayout才是,剩下的都是其他控件的参数。

完整代码传送门

XML中的参数传送门(ArcLayout)

疯狂暗示
疯狂暗示!!!

猜你喜欢

转载自blog.csdn.net/u013599928/article/details/98400360