美文网首页安卓笔记侠
【ImageView】自定义ImageView系列(二)——功能

【ImageView】自定义ImageView系列(二)——功能

作者: 亦枫 | 来源:发表于2015-12-10 23:30 被阅读0次

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

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

    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上的一个优秀例子,介绍一下自定义圆角图片的实现过程,欢迎关注。

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

    微信公众号【技术鸟】_二维码.gif

    相关文章

      网友评论

        本文标题:【ImageView】自定义ImageView系列(二)——功能

        本文链接:https://www.haomeiwen.com/subject/gjjyhttx.html