Android自定义View(五)——带扫描线的View

因为技术原理比较简单,所以就不详细赘述实现的细节了。

效果需求
一个具有圆形背景的等宽等高的视图上,上下来回滚动一个渐变的矩形,矩形的两边不能超出圆,也不能比圆小。

遇到的问题
如果只看上面这句话,大家都会觉得很简单,在视图上绘制一个圆,再绘制一个渐变的矩形,控制坐标来移动矩形就好了。但当大家实际操作的时候可能就会遇到这样一个问题:矩形绘制的时候总感觉很不协调,达不到预期效果。为什么呢?因为android默认的视图轮廓都是矩形的,即使背景是个圆,但是轮廓还是矩形,拟绘制的矩形宽度不变的话,就像一根固定的棍子在圆内滚动,还会超出圆的边界;如果动态设置矩形的宽度,矩形的两端还是不能平滑的和圆重叠。

解决的一些思路
1.如果是Android5.0及以上的系统,这个问题很好解决,有一个方法:View.setClipToOutline(boolean clip)或者在xml里android:clipToOutline =boolean,设置成true后,给view设置一个任何形状的背景,画矩形时把矩形的宽度设得比view个宽度大些或者相等,那么绘制的矩形两端就会很平滑的和圆相切,像是被圆的边盖住了一样;

2.如果是Android5.0以下的系统,也不要灰心,我们可以从裁剪Canvas入手。说到底,View的绘制还是在Canvas上面进行的,只要我们能把Canvas裁剪成一个以view的中心为圆心,以view的宽度的一半或高度的一半为半径的圆,那么不管滚动的矩形多宽,也只能绘制在这个圆形的画板上,多月部分被平滑的截掉了,达到了预期效果。

作为一个自定义view新手,可能这些方法对大牛们而言就是小儿科,但是我却花了一天多时间才折腾出来,大家随意看看就好。

效果图
Android自定义View(五)——带扫描线的View_第1张图片

代码

/**
 * com.ykb.json.customview
 * 描述 :带扫描线的ImageView
 * 作者 : ykb
 * 时间 : 15/11/4.
 */
public class ScanningImageView extends ImageView {

    private static final int CHANGE_BOUNDS = 50;
    private Paint mPaint;
    private int mHeight = 0;
    private Path mPath;

    public ScanningImageView(Context context) {
        this(context, null);
    }

    public ScanningImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ScanningImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            setClipToOutline(true);//设置绘制的覆盖物不能超出背景的轮廓
        }
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.TRANSPARENT);
        mPaint.setAlpha(255);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        mHeight += 10;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            mPath.reset();
            canvas.clipPath(mPath); 
            mPath.addCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2, Path.Direction.CCW);
            canvas.clipPath(mPath, Region.Op.REPLACE);
        }
        LinearGradient linearGradient = new LinearGradient(0, mHeight - CHANGE_BOUNDS, 0, mHeight, new int[]{Color.TRANSPARENT, Color.WHITE}, null, Shader.TileMode.CLAMP);
        mPaint.setShader(linearGradient);
        canvas.drawRect(0, mHeight - CHANGE_BOUNDS, getWidth(), mHeight, mPaint);

        if (mHeight >= getHeight()) {
            mHeight = 0;
        }
        postInvalidateDelayed(40);

        super.onDraw(canvas);
    }
}

======================我是华丽的分割线==========================

因为当时没有适配机型测试,后来发现在三星等手机5.0以下的系统版本上会出现无法裁剪的bug,现在来修正一下以前的做法o(╯□╰)o

1.首先,在5.0以下的手机上,必须把这个视图关闭硬件加速——解决不能正常裁剪的问题;

2.把构造方法里的

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            setClipToOutline(true);//设置绘制的覆盖物不能超出背景的轮廓
        }

去掉;

3.把onDraw方法里的

 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            mPath.reset();
            canvas.clipPath(mPath); 
            mPath.addCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2, Path.Direction.CCW);
            canvas.clipPath(mPath, Region.Op.REPLACE);
        }

if判断去掉

  @Override
    protected void onDraw(Canvas canvas) {
        mHeight += 10;
        mPath.reset();
        canvas.clipPath(mPath); 
        mPath.addCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2, Path.Direction.CCW);
        canvas.clipPath(mPath, Region.Op.REPLACE);
        LinearGradient linearGradient = new LinearGradient(0, mHeight - CHANGE_BOUNDS, 0, mHeight, new int[]{Color.TRANSPARENT, Color.WHITE}, null, Shader.TileMode.CLAMP);
        mPaint.setShader(linearGradient);
        canvas.drawRect(0, mHeight - CHANGE_BOUNDS, getWidth(), mHeight, mPaint);

        if (mHeight >= getHeight()) {
            mHeight = 0;
        }
        postInvalidateDelayed(40);

        super.onDraw(canvas);
    }

执行以上三个操作后,基本可以适配绝大部分机型了。

但如果你认为这样就完了,那还真的没完。。。
后来没事的时候,我有捣鼓了一下以前的一些项目,其中就包括这个扫描ImageView,现在我把它弄得更“臃肿”了——集合了可以裁剪为圆形ImageView的功能:

/**
 * 包名:com.ykb.json.customview
 * 描述:可裁剪为圆形和自带扫描线的ImageView
 * 创建者:yankebin
 * 日期:2015/12/15
 */
public class RoundScanningImageView extends ImageView {
    private Paint mPaint;
    private int mHeight = 0;
    private Path mPath;
    private float centerX;
    private float centerY;

    private float moveSpeed;
    private float outStrokeWidth;
    private int outStrokeColor;
    private int outStrokeAlpha;
    private boolean enableClipPathRound;
    private boolean enableScan;
    private float strokeWidth;
    private float scanLineHeight;
    private int invalidateTime;
    private PorterDuffXfermode porterDuffXfermode;

    public RoundScanningImageView(Context context) {
        this(context, null);
    }

    public RoundScanningImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RoundScanningImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //此处容易抛异常,导致关闭硬件加速失败
        try {
            setLayerType(LAYER_TYPE_SOFTWARE, null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        TypedArray typedArray = getResources().obtainAttributes(attrs, R.styleable.RoundScanningImageView);
        moveSpeed = typedArray.getFloat(R.styleable.RoundScanningImageView_scan_speed, 10);
        outStrokeWidth = typedArray.getFloat(R.styleable.RoundScanningImageView_out_stroke_width, 10);
        outStrokeColor = typedArray.getColor(R.styleable.RoundScanningImageView_out_stroke_color, Color.LTGRAY);
        outStrokeAlpha = typedArray.getInt(R.styleable.RoundScanningImageView_out_stroke_alpha, 100);
        enableClipPathRound = typedArray.getBoolean(R.styleable.RoundScanningImageView_enable_clipPath_round, true);
        enableScan = typedArray.getBoolean(R.styleable.RoundScanningImageView_enable_scan, true);
        strokeWidth = typedArray.getFloat(R.styleable.RoundScanningImageView_stroke_width, 10);
        scanLineHeight = typedArray.getFloat(R.styleable.RoundScanningImageView_scan_line_height, 50);
        invalidateTime = typedArray.getInt(R.styleable.RoundScanningImageView_scan_invalidate_time, 50);
        typedArray.recycle();

        porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
        mPath = new Path();
        mPaint = new Paint();
    }

    /**
     * 创建渲染器
     *
     * @return
     */
    private LinearGradient buildLinearGradient() {
        LinearGradient linearGradient = new LinearGradient(0, mHeight - scanLineHeight, 0, mHeight, new int[]{Color.TRANSPARENT, Color.WHITE}, null, Shader.TileMode.CLAMP);
        return linearGradient;
    }

    /**
     * 画笔重置
     *
     * @param color
     * @param alpha
     */
    private void resetPaint(int color, int alpha) {
        mPaint.reset();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setColor(color);
        mPaint.setAlpha(alpha);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        centerX = getWidth() / 2;
        centerY = getHeight() / 2;
//        ViewParent parent = getParent();
//        if (null != parent && parent instanceof ViewGroup) {
//            ((ViewGroup) parent).setLayerType(LAYER_TYPE_SOFTWARE, null);
//        }
    }

    /**
     * 获取裁剪后的圆形图片
     *
     * @param bmp
     * @param radius
     * @return
     */
    private Bitmap getCroppedRoundBitmap(Bitmap bmp, int radius) {
        Bitmap scaledSrcBmp;
        int diameter = radius * 2;

        // 为了防止宽高不相等,造成圆形图片变形,因此截取长方形中处于中间位置最大的正方形图片
        int bmpWidth = bmp.getWidth();
        int bmpHeight = bmp.getHeight();
        int squareWidth, squareHeight;
        int x, y;
        Bitmap squareBitmap;
        if (bmpHeight > bmpWidth) {// 高大于宽
            squareWidth = squareHeight = bmpWidth;
            x = 0;
            y = (bmpHeight - bmpWidth) / 2;
            // 截取正方形图片
            squareBitmap = Bitmap.createBitmap(bmp, x, y, squareWidth,
                    squareHeight);
        } else if (bmpHeight < bmpWidth) {// 宽大于高
            squareWidth = squareHeight = bmpHeight;
            x = (bmpWidth - bmpHeight) / 2;
            y = 0;
            squareBitmap = Bitmap.createBitmap(bmp, x, y, squareWidth,
                    squareHeight);
        } else {
            squareBitmap = bmp;
        }

        if (squareBitmap.getWidth() != diameter
                || squareBitmap.getHeight() != diameter) {
            scaledSrcBmp = Bitmap.createScaledBitmap(squareBitmap, diameter,
                    diameter, true);

        } else {
            scaledSrcBmp = squareBitmap;
        }
        Bitmap output = Bitmap.createBitmap(scaledSrcBmp.getWidth(),
                scaledSrcBmp.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(output);

        Paint paint = new Paint();
        Rect rect = new Rect(0, 0, scaledSrcBmp.getWidth(),
                scaledSrcBmp.getHeight());

        paint.setAntiAlias(true);
        paint.setFilterBitmap(true);
        paint.setDither(true);
        canvas.drawARGB(0, 0, 0, 0);
        canvas.drawCircle(scaledSrcBmp.getWidth() / 2,
                scaledSrcBmp.getHeight() / 2, scaledSrcBmp.getWidth() / 2,
                paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(scaledSrcBmp, rect, rect, paint);

        return output;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (!enableClipPathRound && !enableScan) {
            super.onDraw(canvas);
        } else {
            if (enableClipPathRound) {
                int radius = getWidth() > getHeight() ? getHeight() / 2 : getWidth() / 2;
                radius -= outStrokeWidth / 2;
                //绘制圆边
                resetPaint(outStrokeColor, outStrokeAlpha);
                mPaint.setStyle(Paint.Style.STROKE);
                mPaint.setStrokeWidth(outStrokeWidth);
                canvas.drawCircle(centerX, centerY, radius, mPaint);
                //处理图片
                Drawable drawable = getDrawable();
                if (null != drawable) {
                    Bitmap mBitmap = ((BitmapDrawable) drawable).getBitmap();
                    if (null != mBitmap) {
                        radius -= strokeWidth;
                        //裁剪图片为圆形
                        Bitmap roundBitmap = getCroppedRoundBitmap(mBitmap, radius);
                        if (null != roundBitmap) {
                            try {
                                canvas.drawBitmap(roundBitmap, centerX - radius, centerY - radius, null);
                                roundBitmap.recycle();
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            } else {
                super.onDraw(canvas);
            }

            if (enableScan) {
                //移动扫描白线的位置
                mHeight += moveSpeed;
                if (enableClipPathRound) {
                    //裁剪画布
                    mPath.reset();
                    canvas.clipPath(mPath); // makes the clip empty
                    mPath.addCircle(centerX, centerY, centerX - strokeWidth - outStrokeWidth / 2, Path.Direction.CCW);
                    canvas.clipPath(mPath, Region.Op.REPLACE);
                }
                //绘制扫描线
                resetPaint(Color.TRANSPARENT, 255);
                mPaint.setXfermode(porterDuffXfermode);
                mPaint.setShader(buildLinearGradient());
                canvas.drawRect(0, mHeight - scanLineHeight, getWidth(), mHeight, mPaint);

                if (mHeight >= getHeight()) {
                    mHeight = 0;
                }
                postInvalidateDelayed(invalidateTime);
            }
        }
    }
}

自定义的属性:

 <declare-styleable name="RoundScanningImageView">
        <attr name="out_stroke_width" format="float"/>
        <attr name="enable_scan" format="boolean"/>
        <attr name="scan_speed" format="float"/>
        <attr name="enable_clipPath_round" format="boolean"/>
        <attr name="stroke_width" format="float"/>
        <attr name="out_stroke_color" format="color"/>
        <attr name="out_stroke_alpha" format="integer"/>
        <attr name="scan_line_height" format="float"/>
        <attr name="scan_invalidate_time" format="integer"/>

    declare-styleable>

=============================再一次优化=============================
优化如下:
引入弱引用缓存相关图片,避免高频率gc

public class RoundScanningImageView extends ImageView {
    private Paint mPaint;
    private int mHeight = 0;
    private Path mPath;
    private float centerX;
    private float centerY;

    private float moveSpeed;
    private float outStrokeWidth;
    private int outStrokeColor;
    private int outStrokeAlpha;
    private boolean enableClipPathRound;
    private boolean enableScan;
    private float strokeWidth;
    private float scanLineHeight;
    private int invalidateTime;
    private PorterDuffXfermode porterDuffXfermode;
    private Drawable mLastDrawable;
    private WeakReference mTempBitmap;
    private WeakReference mLastBitmap;


    public RoundScanningImageView(Context context) {
        this(context, null);
    }

    public RoundScanningImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RoundScanningImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        try {
            setLayerType(LAYER_TYPE_SOFTWARE, null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        TypedArray typedArray = getResources().obtainAttributes(attrs, R.styleable.RoundScanningImageView);
        moveSpeed = typedArray.getFloat(R.styleable.RoundScanningImageView_scan_speed, 10);
        outStrokeWidth = typedArray.getFloat(R.styleable.RoundScanningImageView_out_stroke_width, 10);
        outStrokeColor = typedArray.getColor(R.styleable.RoundScanningImageView_out_stroke_color, Color.LTGRAY);
        outStrokeAlpha = typedArray.getInt(R.styleable.RoundScanningImageView_out_stroke_alpha, 100);
        enableClipPathRound = typedArray.getBoolean(R.styleable.RoundScanningImageView_enable_clipPath_round, true);
        enableScan = typedArray.getBoolean(R.styleable.RoundScanningImageView_enable_scan, true);
        strokeWidth = typedArray.getFloat(R.styleable.RoundScanningImageView_stroke_width, 10);
        scanLineHeight = typedArray.getFloat(R.styleable.RoundScanningImageView_scan_line_height, 50);
        invalidateTime = typedArray.getInt(R.styleable.RoundScanningImageView_scan_invalidate_time, 50);
        typedArray.recycle();

        porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
        mPath = new Path();
        mPaint = new Paint();
    }

    /**
     * 创建渲染器
     *
     * @return
     */
    private LinearGradient buildLinearGradient() {
        LinearGradient linearGradient = new LinearGradient(0, mHeight - scanLineHeight, 0, mHeight, new int[]{Color.TRANSPARENT, Color.WHITE}, null, Shader.TileMode.CLAMP);
        return linearGradient;
    }

    /**
     * 画笔重置
     *
     * @param color
     * @param alpha
     */
    private void resetPaint(int color, int alpha) {
        mPaint.reset();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setColor(color);
        mPaint.setAlpha(alpha);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        centerX = getWidth() / 2;
        centerY = getHeight() / 2;
    }

    /**
     * 获取裁剪后的圆形图片
     *
     * @param bmp
     * @param radius
     * @return
     */
    private Bitmap getCroppedRoundBitmap(Bitmap bmp, int radius) {
        Bitmap scaledSrcBmp;
        int diameter = radius * 2;

        // 为了防止宽高不相等,造成圆形图片变形,因此截取长方形中处于中间位置最大的正方形图片
        int bmpWidth = bmp.getWidth();
        int bmpHeight = bmp.getHeight();
        int squareWidth, squareHeight;
        int x, y;
        Bitmap squareBitmap;
        if (bmpHeight > bmpWidth) {// 高大于宽
            squareWidth = squareHeight = bmpWidth;
            x = 0;
            y = (bmpHeight - bmpWidth) / 2;
            // 截取正方形图片
            squareBitmap = Bitmap.createBitmap(bmp, x, y, squareWidth,
                    squareHeight);
        } else if (bmpHeight < bmpWidth) {// 宽大于高
            squareWidth = squareHeight = bmpHeight;
            x = (bmpWidth - bmpHeight) / 2;
            y = 0;
            squareBitmap = Bitmap.createBitmap(bmp, x, y, squareWidth,
                    squareHeight);
        } else {
            squareBitmap = bmp;
        }

        if (squareBitmap.getWidth() != diameter
                || squareBitmap.getHeight() != diameter) {
            scaledSrcBmp = Bitmap.createScaledBitmap(squareBitmap, diameter,
                    diameter, true);

        } else {
            scaledSrcBmp = squareBitmap;
        }
        Bitmap output = Bitmap.createBitmap(scaledSrcBmp.getWidth(),
                scaledSrcBmp.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(output);

        Paint paint = new Paint();
        Rect rect = new Rect(0, 0, scaledSrcBmp.getWidth(),
                scaledSrcBmp.getHeight());

        paint.setAntiAlias(true);
        paint.setFilterBitmap(true);
        paint.setDither(true);
        canvas.drawARGB(0, 0, 0, 0);
        canvas.drawCircle(scaledSrcBmp.getWidth() / 2,
                scaledSrcBmp.getHeight() / 2, scaledSrcBmp.getWidth() / 2,
                paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(scaledSrcBmp, rect, rect, paint);

        return output;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (!enableClipPathRound && !enableScan) {
            super.onDraw(canvas);
        } else {
            if (enableClipPathRound) {
                int radius = getWidth() > getHeight() ? getHeight() / 2 : getWidth() / 2;
                radius -= outStrokeWidth / 2;
                //绘制圆边
                resetPaint(outStrokeColor, outStrokeAlpha);
                mPaint.setStyle(Paint.Style.STROKE);
                mPaint.setStrokeWidth(outStrokeWidth);
                canvas.drawCircle(centerX, centerY, radius, mPaint);
                //处理图片
                Drawable drawable = getDrawable();
                if (null != drawable) {

                    boolean isSameImg = true;
                    if (null != mLastDrawable) {
                        if (drawable.getConstantState() != mLastDrawable.getConstantState()) {
                            isSameImg = false;
                            mLastDrawable = drawable;
                        }
                    } else {
                        isSameImg = false;
                        mLastDrawable = drawable;
                    }
                    boolean needCreate = false;
                    Bitmap roundBitmap = null;
                    if (isSameImg) {
                        roundBitmap = null == mLastBitmap || null == mLastBitmap.get() ? null : mLastBitmap.get();
                        if (null == roundBitmap) {
                            needCreate = true;
                        }
                    } else {
                        needCreate = true;
                    }
                    if (needCreate) {
                        Bitmap mBitmap = null;
                        //防止引用上一次的缓存原图
                        boolean needCreateTemp=false;
                        if(isSameImg){
                            mBitmap = null == mTempBitmap || null == mTempBitmap.get() ? null : mTempBitmap.get();
                            if (null == mBitmap) {
                                needCreateTemp=true;
                            }
                        }else {
                            needCreateTemp=true;
                        }

                        if(needCreateTemp){
                            mBitmap = ((BitmapDrawable) mLastDrawable).getBitmap();
                            mTempBitmap = new WeakReference<>(mBitmap);
                        }

                        if (null != mBitmap) {
                            radius -= strokeWidth;
                            //裁剪图片为圆形
                            roundBitmap = getCroppedRoundBitmap(mBitmap, radius);
                            if (null != roundBitmap) {
                                mLastBitmap = new WeakReference<>(roundBitmap);
                            }
                        }
                    } else {
                        radius -= strokeWidth;
                    }
                    if (null != roundBitmap) {
                        canvas.drawBitmap(roundBitmap, centerX - radius, centerY - radius, null);
                    }
                }
                //裁剪画布
                mPath.reset();
                canvas.clipPath(mPath); // makes the clip empty
                mPath.addCircle(centerX, centerY, centerX - strokeWidth - outStrokeWidth / 2, Path.Direction.CCW);
                canvas.clipPath(mPath, Region.Op.REPLACE);
            } else {
                super.onDraw(canvas);
            }
            if (enableScan) {
                //移动扫描白线的位置
                mHeight += moveSpeed;
                //绘制扫描线
                resetPaint(Color.TRANSPARENT, 255);
                mPaint.setXfermode(porterDuffXfermode);
                mPaint.setShader(buildLinearGradient());
                canvas.drawRect(0, mHeight - scanLineHeight, getWidth(), mHeight, mPaint);

                if (mHeight >= getHeight()) {
                    mHeight = 0;
                }
                postInvalidateDelayed(invalidateTime);
            }
        }
    }
}

你可能感兴趣的:(android,View,android,技术,自定义View,扫描线,圆形ImageV)