【ImageView】自定义ImageView系列(二)——功能全面的带边框圆形图片

在上一篇文章【ImageView】自定义ImageView系列(一)——简单圆形图片中,我们初步了解了一个圆形ImageView的实现方式,基本上能满足功能的使用。

但是,但是它还不能称之为一个优秀的自定义圆形ImageView。本文要介绍的是GitHub上的开源代码 CircularImageView
https://github.com/Pkmmte/CircularImageView

《【ImageView】自定义ImageView系列(二)——功能全面的带边框圆形图片》 CircularImageView_Preview.gif

相比上一遍简单圆形ImageView控件的实现,CircularImageView的优秀点在于:

  • 对外接口
    作为一个优秀的自定义View, 考虑到在布局中和在代码中两种使用场景,对于View的自定义功能属性,定义在attrs.xml资源文件中,同时在源码中提供属性的setter和getter方法,供外部类调用,以达到依据不同功能使用场景,控制自如。

  • 功能全面
    作为常用的圆形ImageView来讲,提供了设置边框Border的功能,包括基本的Border颜色、宽度、阴影等属性,同时CircularImageView提供了触摸状态下ImageView按压的效果。

  • 性能优化
    自定义View特别需要注意的一点就是性能优化,注意资源的回收,避免内存的泄露,比如TypedArray的recycl,Bitmap的复用等。

Github上提供了Eclipse和Android Studio两个版本,可以下载运行查看效果。其中的java和attrs文件在本文中也有引用介绍,可以直接拿到自己项目使用。其实,看完本文之后,就会发现,只要有一定的自定义View基础,这样的一个控件并不难实现。

接下来展示一下CircularImageView的源码,其中主要地方的注释均已标注,如果看完上一篇的文章,相信基本上也能看懂下面的源码内容。

CircularImageView.java

package com.pkmmte.view;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ImageView;

/**
 * Custom ImageView for circular images in Android while maintaining the
 * best draw performance and supporting custom borders & selectors.
 */
public class CircularImageView extends ImageView {
    // For logging purposes
    private static final String TAG = CircularImageView.class.getSimpleName();

    // Default property values
    private static final boolean SHADOW_ENABLED = false;
    private static final float SHADOW_RADIUS = 4f;
    private static final float SHADOW_DX = 0f;
    private static final float SHADOW_DY = 2f;
    private static final int SHADOW_COLOR = Color.BLACK;

    // Border & Selector configuration variables
    private boolean hasBorder;
    private boolean hasSelector;
    private boolean isSelected;
    private int borderWidth;
    private int canvasSize;
    private int selectorStrokeWidth;

    // Shadow properties
    private boolean shadowEnabled;
    private float shadowRadius;
    private float shadowDx;
    private float shadowDy;
    private int shadowColor;

    // Objects used for the actual drawing
    private BitmapShader shader;
    private Bitmap image;
    private Paint paint;
    private Paint paintBorder;
    private Paint paintSelectorBorder;
    private ColorFilter selectorFilter;

    public CircularImageView(Context context) {
        this(context, null, R.styleable.CircularImageViewStyle_circularImageViewDefault);
    }

    public CircularImageView(Context context, AttributeSet attrs) {
        this(context, attrs, R.styleable.CircularImageViewStyle_circularImageViewDefault);
    }

    public CircularImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public CircularImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context, attrs, defStyleAttr);
    }

    /**
     * Initializes paint objects and sets desired attributes.
     * @param context Context
     * @param attrs Attributes
     * @param defStyle Default Style
     */
    private void init(Context context, AttributeSet attrs, int defStyle) {
        // Initialize paint objects
        paint = new Paint();
        paint.setAntiAlias(true);
        paintBorder = new Paint();
        paintBorder.setAntiAlias(true);
        paintBorder.setStyle(Paint.Style.STROKE);
        paintSelectorBorder = new Paint();
        paintSelectorBorder.setAntiAlias(true);

        // Enable software rendering on HoneyComb and up. (needed for shadow)
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
            setLayerType(LAYER_TYPE_SOFTWARE, null);

        // Load the styled attributes and set their properties
        TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.CircularImageView, defStyle, 0);

        // Check for extra features being enabled
        hasBorder = attributes.getBoolean(R.styleable.CircularImageView_civ_border, false);
        hasSelector = attributes.getBoolean(R.styleable.CircularImageView_civ_selector, false);
        shadowEnabled = attributes.getBoolean(R.styleable.CircularImageView_civ_shadow, SHADOW_ENABLED);

        // Set border properties, if enabled
        if(hasBorder) {
            int defaultBorderSize = (int) (2 * context.getResources().getDisplayMetrics().density + 0.5f);
            setBorderWidth(attributes.getDimensionPixelOffset(R.styleable.CircularImageView_civ_borderWidth, defaultBorderSize));
            setBorderColor(attributes.getColor(R.styleable.CircularImageView_civ_borderColor, Color.WHITE));
        }

        // Set selector properties, if enabled
        if(hasSelector) {
            int defaultSelectorSize = (int) (2 * context.getResources().getDisplayMetrics().density + 0.5f);
            setSelectorColor(attributes.getColor(R.styleable.CircularImageView_civ_selectorColor, Color.TRANSPARENT));
            setSelectorStrokeWidth(attributes.getDimensionPixelOffset(R.styleable.CircularImageView_civ_selectorStrokeWidth, defaultSelectorSize));
            setSelectorStrokeColor(attributes.getColor(R.styleable.CircularImageView_civ_selectorStrokeColor, Color.BLUE));
        }

        // Set shadow properties, if enabled
        if(shadowEnabled) {
            shadowRadius = attributes.getFloat(R.styleable.CircularImageView_civ_shadowRadius, SHADOW_RADIUS);
            shadowDx = attributes.getFloat(R.styleable.CircularImageView_civ_shadowDx, SHADOW_DX);
            shadowDy = attributes.getFloat(R.styleable.CircularImageView_civ_shadowDy, SHADOW_DY);
            shadowColor = attributes.getColor(R.styleable.CircularImageView_civ_shadowColor, SHADOW_COLOR);
            setShadowEnabled(true);
        }

        // We no longer need our attributes TypedArray, give it back to cache
        attributes.recycle();
    }

    /**
     * Sets the CircularImageView's border width in pixels.
     * @param borderWidth Width in pixels for the border.
     */
    public void setBorderWidth(int borderWidth) {
        this.borderWidth = borderWidth;
        if(paintBorder != null)
            paintBorder.setStrokeWidth(borderWidth);
        requestLayout();
        invalidate();
    }

    /**
     * Sets the CircularImageView's basic border color.
     * @param borderColor The new color (including alpha) to set the border.
     */
    public void setBorderColor(int borderColor) {
        if (paintBorder != null)
            paintBorder.setColor(borderColor);
        this.invalidate();
    }

    /**
     * Sets the color of the selector to be draw over the
     * CircularImageView. Be sure to provide some opacity.
     * @param selectorColor The color (including alpha) to set for the selector overlay.
     */
    public void setSelectorColor(int selectorColor) {
        this.selectorFilter = new PorterDuffColorFilter(selectorColor, PorterDuff.Mode.SRC_ATOP);
        this.invalidate();
    }

    /**
     * Sets the stroke width to be drawn around the CircularImageView
     * during click events when the selector is enabled.
     * @param selectorStrokeWidth Width in pixels for the selector stroke.
     */
    public void setSelectorStrokeWidth(int selectorStrokeWidth) {
        this.selectorStrokeWidth = selectorStrokeWidth;
        this.requestLayout();
        this.invalidate();
    }

    /**
     * Sets the stroke color to be drawn around the CircularImageView
     * during click events when the selector is enabled.
     * @param selectorStrokeColor The color (including alpha) to set for the selector stroke.
     */
    public void setSelectorStrokeColor(int selectorStrokeColor) {
        if (paintSelectorBorder != null)
            paintSelectorBorder.setColor(selectorStrokeColor);
        this.invalidate();
    }

    /**
     * Enables a dark shadow for this CircularImageView.
     * @param enabled Set to true to draw a shadow or false to disable it.
     */
    public void setShadowEnabled(boolean enabled) {
        shadowEnabled = enabled;
        updateShadow();
    }

    /**
     * Enables a dark shadow for this CircularImageView.
     * If the radius is set to 0, the shadow is removed.
     * @param radius Radius for the shadow to extend to.
     * @param dx Horizontal shadow offset.
     * @param dy Vertical shadow offset.
     * @param color The color of the shadow to apply.
     */
    public void setShadow(float radius, float dx, float dy, int color) {
        shadowRadius = radius;
        shadowDx = dx;
        shadowDy = dy;
        shadowColor = color;
        updateShadow();
    }

    @Override
    public void onDraw(Canvas canvas) {
        // Don't draw anything without an image
        if(image == null)
            return;

        // Nothing to draw (Empty bounds)
        if(image.getHeight() == 0 || image.getWidth() == 0)
            return;

        // Update shader if canvas size has changed
        int oldCanvasSize = canvasSize;
        canvasSize = getWidth() < getHeight() ? getWidth() : getHeight();
        if(oldCanvasSize != canvasSize)
            updateBitmapShader();

        // Apply shader to paint
        paint.setShader(shader);

        // Keep track of selectorStroke/border width
        int outerWidth = 0;

        // Get the exact X/Y axis of the view
        int center = canvasSize / 2;


        if(hasSelector && isSelected) { // Draw the selector stroke & apply the selector filter, if applicable
            outerWidth = selectorStrokeWidth;
            center = (canvasSize - (outerWidth * 2)) / 2;

            paint.setColorFilter(selectorFilter);
            canvas.drawCircle(center + outerWidth, center + outerWidth, ((canvasSize - (outerWidth * 2)) / 2) + outerWidth - 4.0f, paintSelectorBorder);
        }
        else if(hasBorder) { // If no selector was drawn, draw a border and clear the filter instead... if enabled
            outerWidth = borderWidth;
            center = (canvasSize - (outerWidth * 2)) / 2;

            paint.setColorFilter(null);
            RectF rekt = new RectF(0 + outerWidth / 2, 0 + outerWidth / 2, canvasSize - outerWidth / 2, canvasSize - outerWidth / 2);
            canvas.drawArc(rekt, 360, 360, false, paintBorder);
            //canvas.drawCircle(center + outerWidth, center + outerWidth, ((canvasSize - (outerWidth * 2)) / 2) + outerWidth - 4.0f, paintBorder);
        }
        else // Clear the color filter if no selector nor border were drawn
            paint.setColorFilter(null);

        // Draw the circular image itself
        canvas.drawCircle(center + outerWidth, center + outerWidth, ((canvasSize - (outerWidth * 2)) / 2), paint);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        // Check for clickable state and do nothing if disabled
        if(!this.isClickable()) {
            this.isSelected = false;
            return super.onTouchEvent(event);
        }

        // Set selected state based on Motion Event
        switch(event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                this.isSelected = true;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_SCROLL:
            case MotionEvent.ACTION_OUTSIDE:
            case MotionEvent.ACTION_CANCEL:
                this.isSelected = false;
                break;
        }

        // Redraw image and return super type
        this.invalidate();
        return super.dispatchTouchEvent(event);
    }

    @Override
    public void setImageURI(Uri uri) {
        super.setImageURI(uri);

        // Extract a Bitmap out of the drawable & set it as the main shader
        image = drawableToBitmap(getDrawable());
        if(canvasSize > 0)
            updateBitmapShader();
    }

    @Override
    public void setImageResource(int resId) {
        super.setImageResource(resId);

        // Extract a Bitmap out of the drawable & set it as the main shader
        image = drawableToBitmap(getDrawable());
        if(canvasSize > 0)
            updateBitmapShader();
    }

    @Override
    public void setImageDrawable(Drawable drawable) {
        super.setImageDrawable(drawable);

        // Extract a Bitmap out of the drawable & set it as the main shader
        image = drawableToBitmap(getDrawable());
        if(canvasSize > 0)
            updateBitmapShader();
    }

    @Override
    public void setImageBitmap(Bitmap bm) {
        super.setImageBitmap(bm);

        // Extract a Bitmap out of the drawable & set it as the main shader
        image = bm;
        if(canvasSize > 0)
            updateBitmapShader();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = measureWidth(widthMeasureSpec);
        int height = measureHeight(heightMeasureSpec);
        setMeasuredDimension(width, height);
    }

    private int measureWidth(int measureSpec) {
        int result;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            // The parent has determined an exact size for the child.
            result = specSize;
        }
        else if (specMode == MeasureSpec.AT_MOST) {
            // The child can be as large as it wants up to the specified size.
            result = specSize;
        }
        else {
            // The parent has not imposed any constraint on the child.
            result = canvasSize;
        }

        return result;
    }

    private int measureHeight(int measureSpecHeight) {
        int result;
        int specMode = MeasureSpec.getMode(measureSpecHeight);
        int specSize = MeasureSpec.getSize(measureSpecHeight);

        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else if (specMode == MeasureSpec.AT_MOST) {
            // The child can be as large as it wants up to the specified size.
            result = specSize;
        } else {
            // Measure the text (beware: ascent is a negative number)
            result = canvasSize;
        }

        return (result + 2);
    }

    // TODO: Update shadow layers based on border/selector state and visibility.
    private void updateShadow() {
        float radius = shadowEnabled ? shadowRadius : 0;
        //paint.setShadowLayer(radius, shadowDx, shadowDy, shadowColor);
        paintBorder.setShadowLayer(radius, shadowDx, shadowDy, shadowColor);
        paintSelectorBorder.setShadowLayer(radius, shadowDx, shadowDy, shadowColor);
    }

    /**
     * Convert a drawable object into a Bitmap.
     * @param drawable Drawable to extract a Bitmap from.
     * @return A Bitmap created from the drawable parameter.
     */
    public Bitmap drawableToBitmap(Drawable drawable) {
        if (drawable == null)   // Don't do anything without a proper drawable
            return null;
        else if (drawable instanceof BitmapDrawable) {  // Use the getBitmap() method instead if BitmapDrawable
            Log.i(TAG, "Bitmap drawable!");
            return ((BitmapDrawable) drawable).getBitmap();
        }

        int intrinsicWidth = drawable.getIntrinsicWidth();
        int intrinsicHeight = drawable.getIntrinsicHeight();

        if (!(intrinsicWidth > 0 && intrinsicHeight > 0))
            return null;

        try {
            // Create Bitmap object out of the drawable
            Bitmap bitmap = Bitmap.createBitmap(intrinsicWidth, intrinsicHeight, Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(bitmap);
            drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
            drawable.draw(canvas);
            return bitmap;
        } catch (OutOfMemoryError e) {
            // Simply return null of failed bitmap creations
            Log.e(TAG, "Encountered OutOfMemoryError while generating bitmap!");
            return null;
        }
    }

    // TODO TEST REMOVE
    public void setIconModeEnabled(boolean e) {}

    /**
     * Re-initializes the shader texture used to fill in
     * the Circle upon drawing.
     */
    public void updateBitmapShader() {
        if (image == null)
            return;

        shader = new BitmapShader(image, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

        if(canvasSize != image.getWidth() || canvasSize != image.getHeight()) {
            Matrix matrix = new Matrix();
            float scale = (float) canvasSize / (float) image.getWidth();
            matrix.setScale(scale, scale);
            shader.setLocalMatrix(matrix);
        }
    }

    /**
     * @return Whether or not this view is currently
     * in its selected state.
     */
    public boolean isSelected() {
        return this.isSelected;
    }
}

attrs.xml资源文件

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <!-- CircularImageView Custom Styling -->
    <declare-styleable name="CircularImageView">

        <!-- Whether or not to draw a circular border around the image. -->
        <attr name="civ_border" format="boolean"/>
        <!-- The color of the border draw around the image. (if enabled) -->
        <attr name="civ_borderColor" format="color"/>
        <!-- Makes the border this pixels wide. (if enabled) -->
        <attr name="civ_borderWidth" format="dimension"/>
        <!-- Whether or not to draw a selector on this view upon touch events. -->
        <attr name="civ_selector" format="boolean"/>
        <!-- The color of the selector draw on top of the image upon touch events. (if enabled) -->
        <attr name="civ_selectorColor" format="color"/>
        <!-- The color of the selector stroke drawn around the image upon touch events. Be sure to provide some opacity. (if enabled) -->
        <attr name="civ_selectorStrokeColor" format="color"/>
        <!-- The selector stroke drawn around the image upon touch events this pixels wide. (if enabled) -->
        <attr name="civ_selectorStrokeWidth" format="dimension"/>
        <!-- Whether or not to draw a shadow around your circular image. -->
        <attr name="civ_shadow" format="boolean"/>
        <!-- The radius for the shadow to extend to. (if enabled) -->
        <attr name="civ_shadowRadius" format="float"/>
        <!-- Horizontal shadow offset. (if enabled) -->
        <attr name="civ_shadowDx" format="float"/>
        <!-- Vertical shadow offset. (if enabled) -->
        <attr name="civ_shadowDy" format="float"/>
        <!-- The color of the shadow drawn around your circular image. (if enabled) -->
        <attr name="civ_shadowColor" format="color"/>
    </declare-styleable>

    <declare-styleable name="CircularImageViewStyle">
        <attr name="circularImageViewDefault" format="reference"/>
    </declare-styleable>

</resources>

属性简介

  • app:border (boolean) -> default false
  • app:border_color (color) -> default WHITE
  • app:border_width (dimension) -> default 2dp
  • app:selector (boolean) -> default false
  • app:selector_color (color) -> default TRANSPARENT
  • app:selector_stroke_color (color) -> default BLUE
  • app:selector_stroke_width (dimension) -> default 2dp
  • app:shadow (boolean) -> default false

使用方式

<com.pkmmte.view.CircularImageView
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/imgNetwork"
    android:layout_width="72dp"
    android:layout_height="72dp"
    android:layout_margin="16dp"
    android:src="@drawable/grumpy_cat"
    app:civ_border="true"
    app:civ_borderColor="@android:color/white"
    app:civ_borderWidth="2dp"
    app:civ_shadow="true" />

大致内容如上,可以结合注释仔细阅读一下源码,学习一下所涉及到的自定义View的知识点,以及一个自定义View应具有的基本功能。

下一篇同样以GitHub上的一个优秀例子,介绍一下自定义圆角图片的实现过程,欢迎关注。

关注微信公众号【技术鸟】,掌握最新技术资讯!

《【ImageView】自定义ImageView系列(二)——功能全面的带边框圆形图片》 微信公众号【技术鸟】_二维码.gif

    原文作者:亦枫
    原文地址: https://www.jianshu.com/p/820da06dd828
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞