自定义TextView实现渐变色边框,渐变色文字并绘制drawable

工作需求,渐变色的边框和文字,还需要显示drawable。我们知道如果是View的背景色渐变,那么很好办,只需要写一个drawable文件,里面定义shape然后设置为View的background就行了。但是如果需要渐变色的文字,就得需要重写onDraw方法了,当然渐变色的边框也是这样的。如果重写了onDraw方法,即使设置了drawableLeft、drawableRight等drawable,也是会被覆盖掉的。那么怎么办呢?别急,我来给你一个解决方法。


首先看一下需要实现的效果吧!



说到自定义View,首先:在atts.xml中定义一些属性

<resources>
    <declare-styleable name="BorderTextView">
        <attr name="drawable_src" format="reference"/>
        <attr name="imageHight" format="dimension"/>
        <attr name="imageWidth" format="dimension"/>
        <attr name="imageLocation">
            <enum name="left" value="0"/>
            <enum name="top" value="1"/>
            <enum name="right" value="2"/>
            <enum name="bottom" value="3"/>
        </attr>
    </declare-styleable>
</resources>

第二步:定义一个名为“BorderTextView”的class,当然需要继承自TextView。然后呢?既然定义了属性,那么就得使用。在布局文件中使用

<com.qijukeji.customView.BorderTextView
    android:id="@+id/share_thirdFragment"
    android:layout_width="wrap_content"
    android:layout_height="32dp"
    android:layout_alignParentRight="true"
    android:gravity="center_vertical"
    android:layout_marginTop="8dp"
    android:layout_marginRight="10dp"
    android:textColor="@color/colorPrimary"
    android:drawablePadding="8dp"
    app:drawable_src="@drawable/share_orange_icon"
    app:imageHight="15sp"
    app:imageWidth="15sp"
    app:imageLocation="right"
    />

第三步:布局文件写好了,接下来就该在构造方法中获取到用户用到的参数了。

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

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

public BorderTextView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.BorderTextView,
            defStyleAttr, 0);
    int n = a.getIndexCount();
    for (int i = 0; i < n; i++) {
        int attr = a.getIndex(i);
        switch (attr) {

            case R.styleable.BorderTextView_imageWidth:
                mWidth = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                        TypedValue.COMPLEX_UNIT_SP, 0, getResources().getDisplayMetrics()));
                break;
            case R.styleable.BorderTextView_imageHight:
                mHight = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                        TypedValue.COMPLEX_UNIT_SP, 0, getResources().getDisplayMetrics()));
                break;
            case R.styleable.BorderTextView_drawable_src:
                mImage = BitmapFactory.decodeResource(getResources(), a.getResourceId(attr, 0));
                break;
            case R.styleable.BorderTextView_imageLocation:
                mLocation = a.getInt(attr, LEFT);
                break;
        }
    }
    a.recycle();
}


为了让各个构造方法最终都调用三个参数的构造方法,所以需要稍微修改一下前两个构造方法。


因为我们需要自定义的TextView大小想自己设置,所以必须重写onMeasure方法.

 
 
第四步: 
重写onMeasure方法,设置View的尺寸。 

因为这个TextView里面包含有文字,所以我就用了文字占的宽高再加上图片的宽高设置的View的宽高。图片宽高固定为10sp,这里使用sp是为了和文字统一,因为文字的单位是sp。我设置了文字距离View的左侧留有10sp的距离,图片距离View右侧留有10sp的距离,文字和图片之间的距离是5sp,这样算下来,整个View的宽度就应该是 文字的宽度+图片的宽度10sp+左侧10sp+右侧10sp+中间5sp = 文字宽度+35sp。


宽度定下来了,轮到高度了。高度很简单,我就设置的View的高度是文字的高度+10sp。


需要注意的是:如果动态设置尺寸,就只能使用px作为单位,那么就需要将sp转换为px。方法?

/**
 * sp值转换为px值,保证文字大小不变
 *
 * @param spValue
 * @return
 */
public static int sp2px(Context context, float spValue) {
    final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
    return (int) (spValue * fontScale + 0.5f);
}


文字宽度怎么得到呢?看代码吧!

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    mPaint = getPaint();
    mRect = new Rect();
    String mTipText = getText().toString();
    mPaint.getTextBounds(mTipText, 0, mTipText.length(), mRect);
    int textWidth = mRect.width();
    int textHeight = mRect.height();

    setMeasuredDimension(textWidth + Utils.sp2px(getContext(), 35),
            textHeight + Utils.sp2px(getContext(), 10));

}

这里的getPaint方法可以得到TextPaint对象,然后通过getTextBounds方法,用Rect对象包裹了文字,这样mRect的宽度就是文字的宽度,mRect的高度就是文字的高度。

最后调用setMeasureDimension方法设置View的宽高。



接下来就是重点了,重写onDraw方法。

第五步:重写onDraw方法。

我的思路是先画渐变的文字,然后画图片,最后画边框。

先来绘制文字。

        canvas.save();
        mViewWidth = getMeasuredWidth();
        mViewHeight = getMeasuredHeight();
        mPaint = getPaint();
        mRect = new Rect();
        String mTipText = getText().toString();
        mPaint.getTextBounds(mTipText, 0, mTipText.length(), mRect);


//        关于LinearGradient的使用,
//        推荐网页http://blog.csdn.net/u012702547/article/details/50821044,讲得很明白
        mLinearGradient = new LinearGradient(0, 0, mViewWidth, mViewHeight,
                new int[]{0xFFFFBE7C, 0xFFFF6483},
                null, Shader.TileMode.MIRROR);
        mPaint.setShader(mLinearGradient);

//        -------------------------------绘制不一样大的文字--------------------------------------
        mPaint.setTextSize(Utils.sp2px(getContext(), 11));
        canvas.drawText(mTipText, 0, 1,
                getMeasuredWidth() / 2 - (mRect.width() + Utils.sp2px(getContext(), 15)) / 2,
                getMeasuredHeight() / 2 + mRect.height() / 2, mPaint);
        mPaint.setTextSize(Utils.sp2px(getContext(), 8));
        canvas.drawText(mTipText, 2, 3,
                getMeasuredWidth() / 2 - mRect.width() / 2 + Utils.sp2px(getContext(), 10),
                getMeasuredHeight() / 2 + mRect.height() / 2, mPaint);
        mPaint.setTextSize(Utils.sp2px(getContext(), 16));
        canvas.drawText(mTipText, 3, mTipText.length(),
                getMeasuredWidth() / 2 - mRect.width() / 2 + Utils.sp2px(getContext(), 18),
                getMeasuredHeight() / 2 + mRect.height() / 2, mPaint);
//        -------------------------------------------------------------------------------------

//        -----------------------------绘制一样大的文字------------------------------------------
// canvas.drawText(mTipText,
//          getMeasuredWidth() / 2 - (mRect.width() + Utils.sp2px(getContext(), 15)) / 2,
//          getMeasuredHeight() / 2 + mRect.height() / 2, mPaint);
//        -------------------------------------------------------------------------------------
        canvas.restore();


最开始我是设置了文字大小一样,那么代码就是注释掉的那部分。后来该需求,设置字体不一样大了,所以重新写

首先说一下颜色渐变,使用到了LinearGradient类,这个类支持线性渐变。参数的含义来看一下源码:

/** Create a shader that draws a linear gradient along a line.
    @param x0           The x-coordinate for the start of the gradient line
    @param y0           The y-coordinate for the start of the gradient line
    @param x1           The x-coordinate for the end of the gradient line
    @param y1           The y-coordinate for the end of the gradient line
    @param  colors      The colors to be distributed along the gradient line
    @param  positions   May be null. The relative positions [0..1] of
                        each corresponding color in the colors array. If this is null,
                        the the colors are distributed evenly along the gradient line.
    @param  tile        The Shader tiling mode
*/
public LinearGradient(float x0, float y0, float x1, float y1, int colors[], float positions[],
        TileMode tile) {
    if (colors.length < 2) {
        throw new IllegalArgumentException("needs >= 2 number of colors");
    }
    if (positions != null && colors.length != positions.length) {
        throw new IllegalArgumentException("color and position arrays must be of equal length");
    }
    mType = TYPE_COLORS_AND_POSITIONS;
    mX0 = x0;
    mY0 = y0;
    mX1 = x1;
    mY1 = y1;
    mColors = colors;
    mPositions = positions;
    mTileMode = tile;
    init(nativeCreate1(x0, y0, x1, y1, colors, positions, tile.nativeInt));
}
前四个参数分别是渲染的起始x、y和结束的x、y,这几个参数也表示了颜色渐变的方向是从左上角渐变到右下角。第五个参数是颜色值,线性渐变的话只需要给两个颜色值就可以了,一个起始颜色一个结束颜色。第六个参数是相对位置,当传入两个颜色值的时候直接传入null即可,如果给的颜色值比较多,那么positions的size需要跟colors的size一样,表示在positions[0]的颜色值colors[0], 在positions[1]的颜色值colors[1],以此类推。第七个参数是渐变的模式,有三种:
Shader.TileMode.CLAMP、	//当渐变到结束的颜色之后,后面的颜色将一直使用endColor
Shader.TileMode.REPEAT、//当渐变到结束的颜色之后,会重复从startColor到endColor
Shader.TileMode.MIRROR //当渐变到结束的颜色之后,会镜像的重复颜色值

Paint对象的setShader方法允许我们使用渐变来绘制。

最后调用drawText就可以了。


接下来介绍一下6各参数的drawText方法。

/**
 * Draw the text, with origin at (x,y), using the specified paint.
 * The origin is interpreted based on the Align setting in the paint.
 *
 * @param text  The text to be drawn
 * @param start The index of the first character in text to draw
 * @param end   (end - 1) is the index of the last character in text to draw
 * @param x     The x-coordinate of the origin of the text being drawn
 * @param y     The y-coordinate of the baseline of the text being drawn
 * @param paint The paint used for the text (e.g. color, size, style)
 */
public void drawText(@NonNull String text, int start, int end, float x, float y,
        @NonNull Paint paint) {
    if ((start | end | (end - start) | (text.length() - end)) < 0) {
        throw new IndexOutOfBoundsException();
    }
    native_drawText(mNativeCanvasWrapper, text, start, end, x, y, paint.mBidiFlags,
            paint.getNativeInstance(), paint.mNativeTypeface);
}
这个方法允许我们绘制text的一部分。第一个参数是要绘制的text,第二三个参数是要绘制的起始下标和结束下标,第四五个参数是想绘制在画布的哪个位置,最后一个参数是画笔对象。


第六步:绘制图片drawable。

canvas.save();
drawPicture(canvas);//设置图片方法
canvas.restore();
drawPicture的内容:

    private void drawPicture(Canvas canvas) {
        if (mImage != null) {
            Drawable mDrawable;
            if (mHight != 0 && mWidth != 0) {
                mDrawable = new BitmapDrawable(getResources(), getRealBitmap(mImage));
            } else {
                mDrawable = new BitmapDrawable(getResources(), Bitmap.createScaledBitmap(mImage,
                        mImage.getWidth(), mImage.getHeight(), true));
            }

            mDrawable.setBounds(mRect.width() + Utils.sp2px(getContext(), 15),
                    getMeasuredHeight() / 2 - Utils.sp2px(getContext(), 6),
                    mRect.width() + Utils.sp2px(getContext(), 25),
                    getMeasuredHeight() / 2 + Utils.sp2px(getContext(), 6));
            mDrawable.draw(canvas);


//            switch (mLocation) {
//                case LEFT:
//                    this.setCompoundDrawablesWithIntrinsicBounds(mDrawable, null,
//                            null, null);
//                    break;
//                case TOP:
//                    this.setCompoundDrawablesWithIntrinsicBounds(null, mDrawable,
//                            null, null);
//                    break;
//                case RIGHT:
//                    this.setCompoundDrawablesWithIntrinsicBounds(null, null,
//                            mDrawable, null);
//                    break;
//                case BOTTOM:
//                    this.setCompoundDrawablesWithIntrinsicBounds(null, null, null,
//                            mDrawable);
//                    break;
//            }
        }
    }

getRealBitmap是用来获取缩略图的,因为直接获取到的bitmap可能尺寸不合适,所以需要进行缩放。内容:

private Bitmap getRealBitmap(Bitmap image) {
    //根据需要Drawable原来的大小和目标宽高进行裁剪(缩放)
    int width = image.getWidth();// 获得图片的宽高
    int height = image.getHeight();
    // 取得想要缩放的matrix参数
    float scaleWidth = (float) Utils.sp2px(getContext(), 10) / width;
    float scaleHeight = (float) Utils.sp2px(getContext(), 10) / height;
    Matrix matrix = new Matrix();
    matrix.postScale(scaleWidth, scaleHeight);
    // 返回新的Bitmap
    return Bitmap.createBitmap(image, 0, 0, width, height, matrix, true);
}

在drawPicture方法中我先得到了要画的图片,然后调用setBounds方法设置了图片要绘制的top、left、bottom、right。可以理解为图片距离View的上下左右的距离。

然后调用draw方法画在画布上就行了。


第七步:绘制渐变色边框。

canvas.save();
mPaint = new Paint();
mRect = canvas.getClipBounds();
mPaint.setAntiAlias(true);
mRect.bottom--;
mRect.right--;


mPaint.setStyle(Paint.Style.STROKE);
//设置边框宽度
mPaint.setStrokeWidth(Utils.dip2Px(getContext(), 2));
mPaint.setShader(mLinearGradient);
RectF rectF = new RectF(mRect);
canvas.drawRoundRect(rectF, 12, 12, mPaint);
canvas.restore();

这里我们就需要重新初始化Paint和Rect对象了。注意在绘制边框的时候mRect是使用画布来获取clipBounds,注意与绘制文字的时候的不同。

设置好画笔空心、画笔宽度、然后就可以为画笔设置渐变的LinearGradient对象了。绘制矩形很简单,直接就是

canvas.drawRect(mRect,mPaint);
传入的第一个参数是Rect对象。


但是我现在需要绘制的是圆角矩形,就需要调用canvas.drawRoundRect方法来绘制了,但是找了一下,没有找到直接使用Rect对象来绘制的方法,但是有一个用RectF对象来绘制的方法,很好,我们需要用mRect来构造RectF对象。

RectF rectF = new RectF(mRect);
drawRoundRect方法的第二三个参数是圆角的x半径和y半径。



资源已经上传,想要看详细内容的请下载查看。地址:自定义BorderTextView


---------------------------------------------------------------------------------------------------Bug更新------------------------------------------------------------------------------------------------------------------


以上代码在初次运行没有问题,但是如果刷新了textview(setEnable等方法都会刷新),里面的字体还有图片的位置都会发还是能变化。

打印log查看各种宽度的值后发现,初次得到的包裹字体的Rect对象的宽高跟刷新后的不一样。仔细想了一下,猜测是因为onDraw里面设置的文字大小不一样,位置也不一样,第一次加载textview时是按照字体大小一样距离一样进行加载的,但是记载完后对字体位置和大小改变了,所以包裹字体的Rect的大小也发生变化了。因此,为修复此Bug,我统一使用第一次加载时的宽高进行重绘,这样就能保证刷新也不会出现这种情况。


因为onMeasure方法在onDraw方法前被调用,而我在onMeasure里面获取过一次包裹字体的Rect,因此直接得到的宽高设置成全局变量,以后避免修改这次得到的宽高,在使用这个宽高时直接调用已经保存的,而不是再次获取,这样就达到目的了。


将BorderTextView的onDraw和drawPicture方法中的mRect.width()和mRect.height()全都换成保存好的宽高,测试,OK,解决!


我可不会告诉你修改版的BorderTextView.java在这里哦!



  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值