简化高仿以及源码分析Android 5.0的CardView

2024-02-14 05:08

本文主要是介绍简化高仿以及源码分析Android 5.0的CardView,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

需求: 为了实现定制化的CardView效果,想要定制每一个角落都是圆弧或者直角的需求,需要了解CardView的绘制原理。


 CardView核心思想:

像版本控制就不讲了,只分析如何绘制圆角和阴影的,以下是源码的注释,整体看起来很复杂,但核心步骤就几行代码:

主要涉及这个类: RoundRectDrawableWithShadow

    public void draw(Canvas canvas) {// 开始绘制drawable,mDirty为了防止两次绘制,影响效率,在编程的时候,可以借鉴,没有变化就不要再次计算和绘制了。
        if(this.mDirty) {this.buildComponents(this.getBounds());// 设置内容区域和阴影区域,最好是单步调试一下,看看各个坐标
            this.mDirty = false;
        }canvas.translate(0.0F, this.mRawShadowSize / 2.0F);
        this.drawShadow(canvas);
        canvas.translate(0.0F, -this.mRawShadowSize / 2.0F);
        sRoundRectHelper.drawRoundRect(canvas, this.mCardBounds, this.mCornerRadius, this.mPaint);
    }// 这个是核心,在构造了绘制的封闭扇形区域以后,绘制左上角的情况,之后再通过移动画布,旋转画布,构造4个角的效果。
    private void drawShadow(Canvas canvas) {float edgeShadowTop = -this.mCornerRadius - this.mShadowSize;
        float inset = this.mCornerRadius + this.mInsetShadow + this.mRawShadowSize / 2.0F;
        boolean drawHorizontalEdges = this.mCardBounds.width() - 2.0F * inset > 0.0F;
        boolean drawVerticalEdges = this.mCardBounds.height() - 2.0F * inset > 0.0F;
        int saved = canvas.save();
        canvas.translate(this.mCardBounds.left + inset, this.mCardBounds.top + inset);
        canvas.drawPath(this.mCornerShadowPath, this.mCornerShadowPaint);   // 绘制圆角的阴影
        if(drawHorizontalEdges) {// 绘制边线的阴影
            canvas.drawRect(0.0F, edgeShadowTop, this.mCardBounds.width() - 2.0F * inset, -this.mCornerRadius, this.mEdgeShadowPaint);
        }canvas.restoreToCount(saved);
        saved = canvas.save();
        canvas.translate(this.mCardBounds.right - inset, this.mCardBounds.bottom - inset);
        canvas.rotate(180.0F);
        canvas.drawPath(this.mCornerShadowPath, this.mCornerShadowPaint);
        if(drawHorizontalEdges) {canvas.drawRect(0.0F, edgeShadowTop, this.mCardBounds.width() - 2.0F * inset, -this.mCornerRadius + this.mShadowSize, this.mEdgeShadowPaint);
        }canvas.restoreToCount(saved);

        saved = canvas.save();
        canvas.translate(this.mCardBounds.left + inset, this.mCardBounds.bottom - inset);
        canvas.rotate(270.0F);
        canvas.drawPath(this.mCornerShadowPath, this.mCornerShadowPaint);
        if(drawVerticalEdges) {canvas.drawRect(0.0F, edgeShadowTop, this.mCardBounds.height() - 2.0F * inset, -this.mCornerRadius, this.mEdgeShadowPaint);
        }canvas.restoreToCount(saved);
        // 这个注释的意思是,不明白原理的时候,可以通过删除改变源码,来看理解的效果,这个方法也很重要。
//        saved = canvas.save();
//        canvas.translate(this.mCardBounds.right - inset, this.mCardBounds.top + inset);
//        canvas.rotate(90.0F);
//        canvas.drawPath(this.mCornerShadowPath, this.mCornerShadowPaint);
//        if(drawVerticalEdges) {
//            canvas.drawRect(0.0F, edgeShadowTop, this.mCardBounds.height() - 2.0F * inset, -this.mCornerRadius, this.mEdgeShadowPaint);
//        }
//
//        canvas.restoreToCount(saved);
    }private void buildShadowCorners() {// 仔细看,是负值,往左往外构造了一个内部区域,记住这个半径
        RectF innerBounds = new RectF(-this.mCornerRadius, -this.mCornerRadius, this.mCornerRadius, this.mCornerRadius);
        RectF outerBounds = new RectF(innerBounds);
        outerBounds.inset(-this.mShadowSize, -this.mShadowSize);    // 往外又扩大了一层区域,就是想构造两个弧形中间夹杂的一个扇形区域,用path的形式连接起来,这个就是阴影的区域
        if(this.mCornerShadowPath == null) {this.mCornerShadowPath = new Path();
        } else {this.mCornerShadowPath.reset();
        }this.mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD);
        this.mCornerShadowPath.moveTo(-this.mCornerRadius, 0.0F);   // 可以跟踪坐标,自己画个图,就知道了区域,移动到一个坐标点
        this.mCornerShadowPath.rLineTo(-this.mShadowSize, 0.0F);    // 基于上一个点,往右连接一条线
        this.mCornerShadowPath.arcTo(outerBounds, 180.0F, 90.0F, false);    // 基于右边连线后,绘制一个圆弧,180->转到90度绘制圆弧,
        this.mCornerShadowPath.arcTo(innerBounds, 270.0F, -90.0F, false);   // 记住上边的方向,是基于最后一个点来连接新的弧,这个是270度往左下回来绘制的
        this.mCornerShadowPath.close();     // 构造一个封闭的扇形区域
        float startRatio = this.mCornerRadius / (this.mCornerRadius + this.mShadowSize);    // 这个是非常有技巧的,记住这个比例,因为是扇形上绘制阴影,从扇形的内边开始绘制,到扇形的
        // 外边结束,而这两个扇形的半径是知道的,所以可以计算比例,这个比例是在圆形渐变使用的,从哪里开始渐变,这个非常重要。
        this.mCornerShadowPaint.setShader(new RadialGradient(0.0F, 0.0F, this.mCornerRadius + this.mShadowSize, new int[]{this.mShadowStartColor, this.mShadowStartColor, this.mShadowEndColor}, new float[]{0.0F, startRatio, 1.0F}, Shader.TileMode.CLAMP));
        // 这个是线性渐变,可以比如最下边的阴影就是线性渐变设置的
        this.mEdgeShadowPaint.setShader(new LinearGradient(0.0F, -this.mCornerRadius + this.mShadowSize, 0.0F, -this.mCornerRadius - this.mShadowSize, new int[]{this.mShadowStartColor, this.mShadowStartColor, this.mShadowEndColor}, new float[]{0.0F, 0.5F, 1.0F}, Shader.TileMode.CLAMP));
    }private void buildComponents(Rect bounds) {float verticalOffset = this.mMaxShadowSize * 1.5F;  // 设置阴影的垂直方向的距离,1.5是经验值,我感觉可以根据UI的经验来调节
        // 这个就是内容区域,往里缩小了一些,给阴影部分流出空间
        this.mCardBounds.set((float)bounds.left + this.mMaxShadowSize, (float)bounds.top + verticalOffset, (float)bounds.right - this.mMaxShadowSize, (float)bounds.bottom - verticalOffset);
        // 计算阴影的圆角,这部分比较核心,也是有技巧的,仔细看注释
        this.buildShadowCorners();
    }

我的cardview的效果:


我的cardview的源码:

public class CardFramelayout extends View {private Paint mShaderPaint;
    private Paint mStrokePaint;
    private Paint mContentPaint;
    private Paint mBgPaint;

    private int mColorStart = Color.parseColor("#37000000");
    private int mColorEnd   = Color.parseColor("#03000000");
    private int mColorHorizentalStart = Color.parseColor("#eeeeee");
    private int mColorHorizentalEnd   = Color.parseColor("#ffffffff");
    private int []mColors = new int[] { Color.parseColor("#eeeeee"), Color.parseColor("#efefef"), Color.parseColor("#f0f0f0"),
                                        Color.parseColor("#f1f1f1"), Color.parseColor("#f2f2f2"), Color.parseColor("#f3f3f3"),
                                        Color.parseColor("#f4f4f4"), Color.parseColor("#f5f5f5"), Color.parseColor("#f6f6f6"),
                                        Color.parseColor("#f7f7f7"), Color.parseColor("#f8f8f8"),Color.parseColor("#f9f9f9"),
                                        Color.parseColor("#fefefe")};



    private float [] mLocations;

    private int offset = 40;
    private int radius = 60;
    private int mWidth;
    private int mHeight;
    private Rect mRect, mContentRect, mShaderRect, mLeftRect, mRightRect;
    private LinearGradient mLinearGradient, mLeftGradient, mRightGradient;

    private Rect mRectLeftBottom;
    private Paint mLeftBottomPaint, mLeftPaint;
    private RadialGradient mLeftBottomGradient;

    public CardFramelayout(@NonNull Context context) {super(context);
        init ();
    }public CardFramelayout(@NonNull Context context, @Nullable AttributeSet attrs) {super(context, attrs);
        init ();
    }public CardFramelayout(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {super(context, attrs, defStyleAttr);
        init ();
    }private void init () {mShaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mShaderPaint.setStyle(Paint.Style.FILL);
        mShaderPaint.setColor(Color.BLUE);

        mLeftPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mLeftPaint.setStyle(Paint.Style.FILL);
        mLeftPaint.setColor(Color.BLUE);

        mContentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mContentPaint.setStyle(Paint.Style.FILL);
        mContentPaint.setColor(Color.WHITE);

        mLeftBottomPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mLeftBottomPaint.setStyle(Paint.Style.FILL);
        mLeftBottomPaint.setColor(Color.WHITE);

        mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBgPaint.setStyle(Paint.Style.FILL);
        mBgPaint.setColor(Color.parseColor("#eeeeee"));

        mStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mStrokePaint.setStyle(Paint.Style.STROKE);
        mStrokePaint.setColor(Color.RED); // mColors[0]
        mStrokePaint.setStrokeWidth(3);

        mLocations = new float[mColors.length];
        float step = 1.0f / mColors.length;
        for (int i = 0; i < mColors.length; i++) {mLocations[i] = step * i;
        }}@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;

        int bigRadius = offset + radius;
        int centerX = bigRadius;
        int centerY = mHeight - bigRadius;

        mRect = new Rect (0, 0, w, h);
        mContentRect = new Rect (offset, offset, w - offset, h - offset);
        mShaderRect = new Rect (centerX, h - offset, w , h);

        mLeftRect = new Rect (0, offset, offset, centerY);

        int contentWidth = w - offset * 2;
        mRightRect = new Rect (offset + contentWidth, offset, w, h - offset);

        // 绘制线性渐变,bottom的
        mLinearGradient     = new LinearGradient(offset , h - offset, offset, h, new int[]{mColorStart, mColorEnd}, new float[] {0, 1}, Shader.TileMode.CLAMP);
//        mLeftGradient       = new LinearGradient(0, offset, offset, offset, new int[]{mColorHorizentalEnd, mColorStart}, new float[] {0, 1}, Shader.TileMode.CLAMP);
//        mRightGradient      = new LinearGradient(offset + contentWidth, offset, w, offset, new int[]{mColorStart, mColorHorizentalEnd}, new float[] {0, 1}, Shader.TileMode.CLAMP);
        // 绘制左边的线性渐变
        mLeftGradient       = new LinearGradient(0, offset, offset, offset, new int[]{mColorEnd, mColorStart}, new float[] {0, 1}, Shader.TileMode.CLAMP);
//        mLeftGradient       = new LinearGradient(0, offset, offset, offset, new int[]{mColorStart, mColorEnd}, new float[] {0, 1}, Shader.TileMode.CLAMP);
        mRightGradient      = new LinearGradient(offset + contentWidth, offset, w, offset, new int[]{mColorHorizentalStart, mColorHorizentalEnd}, new float[] {0, 1}, Shader.TileMode.CLAMP);
//        mLinearGradient = new LinearGradient(0, h - offset, 0, h, mColors, mLocations, Shader.TileMode.CLAMP);
        mLeftPaint.setShader(mLeftGradient);
        mShaderPaint.setShader(mLinearGradient);

        mRectLeftBottom = new Rect (0, h - offset * 2, offset * 2, h);

//        int bigRadius = offset + radius;
//        int centerX = bigRadius;
//        int centerY = mHeight - bigRadius;

//        mLeftBottomGradient = new RadialGradient(offset , h - offset, offset, new int[]{Color.RED, Color.GREEN}, new float[] {0, 1}, Shader.TileMode.CLAMP);
        // 这个比较核心,当绘制弧形渐变的时候,要设置两个圆形的比例,再绘制圆形就行了。
        float startRatio = radius * 1.0f / bigRadius;
        mLeftBottomGradient = new RadialGradient(centerX, centerY, bigRadius, new int[]{Color.TRANSPARENT, mColorStart, mColorEnd}, new float[] {0, startRatio, 1}, Shader.TileMode.CLAMP);
//        mLeftBottomGradient = new RadialGradient(offset , h - offset, offset, new int[]{mColorStart, mColorEnd}, new float[] {0, 1}, Shader.TileMode.CLAMP);
        mLeftBottomPaint.setShader(mLeftBottomGradient);
    }Path path = new Path();
    Path shaderPath = new Path();

    @Override
    protected void onDraw(Canvas canvas) {super.onDraw(canvas);

//        canvas.drawRect(0, 0, mWidth, mHeight, mBgPaint);

//        canvas.drawRect(mRectLeftBottom, mLeftBottomPaint);

//        canvas.drawRect(mContentRect, mContentPaint);

        mShaderPaint.setShader(mLinearGradient);
        canvas.drawRect(mShaderRect, mShaderPaint);


        canvas.drawRect(mLeftRect, mLeftPaint);


        int bigRadius = offset + radius;
        int centerX = bigRadius;
        int centerY = mHeight - bigRadius;
        path.moveTo(centerX, centerY);

        // 绘制弧形 center (bigRadius, mHeight - bigRadius);
        RectF arc = new RectF ();
        int l = bigRadius - radius;
        int t = mHeight - bigRadius - radius;
        int r = bigRadius + radius;
        int b = mHeight - bigRadius + radius;
        arc.set(l, t, r, b);
//        canvas.drawArc(arc, 90, 90, true, mStrokePaint);

//        shaderPath.setFillType(Path.FillType.EVEN_ODD);
        shaderPath.reset();
        shaderPath.moveTo(0, centerY);
        shaderPath.lineTo(offset, centerY);

//        mStrokePaint.setStrokeWidth(20);
//        canvas.drawPoint(offset, mHeight - centerY, mStrokePaint);
//        mStrokePaint.setStrokeWidth(4);

        shaderPath.addArc(arc, 180, -90);

        shaderPath.moveTo(centerX, mHeight - offset);
        shaderPath.lineTo(centerX, mHeight);

        RectF outRect = new RectF();
        outRect.set(centerX - bigRadius, centerY - bigRadius, centerX + bigRadius, centerY + bigRadius);
        shaderPath.addArc(outRect, 90, 90);
        shaderPath.moveTo(offset, centerY);
        shaderPath.close();
//        mStrokePaint.setShader(mLeftBottomGradient);
//        canvas.drawPath(shaderPath, mStrokePaint); // mLeftBottomPaint   mStrokePaint

        RectF circle = new RectF();
        circle.set(0, mHeight - centerY * 2, centerY * 2, mHeight);
        canvas.drawArc(outRect, 90, 90, true, mLeftBottomPaint);

//        mShaderPaint.setShader(mRightGradient);
//        canvas.drawRect(mRightRect, mShaderPaint);

    }
}

这篇关于简化高仿以及源码分析Android 5.0的CardView的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/707567

相关文章

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

MOLE 2.5 分析分子通道和孔隙

软件介绍 生物大分子通道和孔隙在生物学中发挥着重要作用,例如在分子识别和酶底物特异性方面。 我们介绍了一种名为 MOLE 2.5 的高级软件工具,该工具旨在分析分子通道和孔隙。 与其他可用软件工具的基准测试表明,MOLE 2.5 相比更快、更强大、功能更丰富。作为一项新功能,MOLE 2.5 可以估算已识别通道的物理化学性质。 软件下载 https://pan.quark.cn/s/57

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

android-opencv-jni

//------------------start opencv--------------------@Override public void onResume(){ super.onResume(); //通过OpenCV引擎服务加载并初始化OpenCV类库,所谓OpenCV引擎服务即是 //OpenCV_2.4.3.2_Manager_2.4_*.apk程序包,存