Android进阶:十二、最简单的方式实现自定义阴影效果

网话说UI设计有三宝 :透明,阴影,加圆角。很多UI在做设计的时候都喜欢做卡片形式,然后添加阴影。卡片UI确实挺好看,但是对Android开发者来说,显示阴影却并不那么手到擒来,因为Android对阴影没有做出很好的支持。

CardView

谷歌也许早就注意到了UI的三宝之一阴影,于是开发了一个继承FrameLayout的CardView公开发这使用,这个控件虽然在v7包里,但是需要单独添加依赖才可以使用,就好像不是亲生的似的!
CardView本质上继承FrameLayout,需要添加依赖才可以使用:

compile ‘com.android.support:cardview-v7:25.3.1‘

当你知道它继承FrameLayout的时候你就知道怎么使用了,但是这个CardView有很多局限性,比如不能修改阴影的颜色,不能修改阴影的深浅。这就很诡异了,根本无法满足UI设计潮流的内心。
那为了产品蒸蒸日上,早日走上人生巅峰,实现财富自由,应该如何让你的APP支持修改阴影的颜色呢?
有个很暴力的办法,就是吧CardView的代码自己抠出来,然后自己定制.
但是现在我自己探索了一个新的较为简单的添加阴影的实现方案,仅供参考


ShadowCardView

思路:首先要明确阴影的实现思路是什么,其实就是颜色导致的视觉错觉。说白了就是在你的Card周围画一个渐变的体现立体感的颜色。
基于上述思路,我们在一个在一个view上画一个矩形的图形,让他周围有渐变色的阴影即可。于是我们想起几个API:

  • 类:Paint 用于在Android上画图的类,相当于画笔
  • 类:Canvas 相当于画布,Android上的view的绘制都与他相关
  • 方法:paint.setShadowLayer可以给绘制的图形增加阴影,还可以设置阴影的颜色

如上图我们,红色部分使我们绘制的图形,边框以内,红色之外的是阴影的显示部分。
有了以上的知识点已经知道可以自己画阴影了,但是什么时机开始画呢?

我们知道谷歌开发的CardView的控件继承了FrameLayout,方便我们自由扩展。那么我们也需要继承FrameLayout。

ShadowCardView继承FrameLayout之后,可以重写其一个方法:

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

这个方法是ViewGroup在绘制子View的时候调用的,那么我们可以在这个时候进行阴影的绘制。

首先,这个方法已经为我们提供了这个View的画布:Canvas,我们可以直接在上面进行阴影的绘制,代码如下:

 Paint shadowPaint = new Paint();
    shadowPaint.setColor(Color.RED);
    shadowPaint.setStyle(Paint.Style.FILL);
    shadowPaint.setAntiAlias(true);
    float left = 45;
    float top = 45;
    float right = getWidth() - 45;
    float bottom = getHeight() - 45;

    shadowPaint.setShadowLayer(45, 0, 0, getContext().getResources().getColor(R.color.color_000000));
    RectF rectF = new RectF(left, top, right, bottom);
    canvas.drawRoundRect(rectF, 0, 0, shadowPaint);
    canvas.save();
  • 创建画笔,设置画笔的颜色,风格
  • 获取绘画的范围:ShadowCard的范围减去需要的阴影的范围,假- - 如阴影的宽度为45px,则在ShadowCard内部的45px内进行绘制
  • 阴影的模糊度,模糊度值越大越模糊,且不能为0
  • 创建RectF,最后开始绘画。

这样阴影就可以成功绘制了,这个方法代码量很少,很简单,也很实用。

效果如上图

为了更好的封装,我们可以为上面需要的参数进行定制,比如阴影的颜色,阴影的宽度,阴影的上下偏移,阴影的模糊度。
全部代码:

public class ShadowViewCard extends FrameLayout {
    private static final int DEFAULT_VALUE_SHADOW_COLOR = R.color.shadow_default_color;
    private static final int DEFAULT_VALUE_SHADOW_CARD_COLOR = R.color.shadow_card_default_color;
    private static final int DEFAULT_VALUE_SHADOW_ROUND = 0;

    private static final int DEFAULT_VALUE_SHADOW_RADIUS = 10;
    private static final int DEFAULT_VALUE_SHADOW_TOP_HEIGHT = 5;
    private static final int DEFAULT_VALUE_SHADOW_LEFT_HEIGHT = 5;
    private static final int DEFAULT_VALUE_SHADOW_RIGHT_HEIGHT = 5;
    private static final int DEFAULT_VALUE_SHADOW_BOTTOM_HEIGHT = 5;
    private static final int DEFAULT_VALUE_SHADOW_OFFSET_Y = 0;
    private static final int DEFAULT_VALUE_SHADOW_OFFSET_X = DEFAULT_VALUE_SHADOW_TOP_HEIGHT / 3;

    private int shadowRound;
    private int shadowColor;
    private int shadowCardColor;
    private int shadowRadius;
    private int shadowOffsetY;
    private int shadowOffsetX;
    private int shadowTopHeight;
    private int shadowLeftHeight;
    private int shadowRightHeight;
    private int shadowBottomHeight;

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

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

    public ShadowViewCard(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context, attrs);
    }

    private void initView(Context context, AttributeSet attrs) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ShadowViewCard);
        shadowRound = a.getDimensionPixelSize(R.styleable.ShadowViewCard_shadowRound, DEFAULT_VALUE_SHADOW_ROUND);
        shadowColor = a.getColor(R.styleable.ShadowViewCard_shadowColor, getResources().getColor(DEFAULT_VALUE_SHADOW_COLOR));
        shadowCardColor = a.getColor(R.styleable.ShadowViewCard_shadowCardColor, getResources().getColor(DEFAULT_VALUE_SHADOW_CARD_COLOR));
        shadowTopHeight = a.getDimensionPixelSize(R.styleable.ShadowViewCard_shadowTopHeight, dp2px(getContext(), DEFAULT_VALUE_SHADOW_TOP_HEIGHT));
        shadowRightHeight = a.getDimensionPixelSize(R.styleable.ShadowViewCard_shadowRightHeight, dp2px(getContext(), DEFAULT_VALUE_SHADOW_RIGHT_HEIGHT));
        shadowLeftHeight = a.getDimensionPixelSize(R.styleable.ShadowViewCard_shadowLeftHeight, dp2px(getContext(), DEFAULT_VALUE_SHADOW_LEFT_HEIGHT));
        shadowBottomHeight = a.getDimensionPixelSize(R.styleable.ShadowViewCard_shadowBottomHeight, dp2px(getContext(), DEFAULT_VALUE_SHADOW_BOTTOM_HEIGHT));
        shadowOffsetY = a.getDimensionPixelSize(R.styleable.ShadowViewCard_shadowOffsetY, dp2px(getContext(), DEFAULT_VALUE_SHADOW_OFFSET_Y));
        shadowOffsetX = a.getDimensionPixelSize(R.styleable.ShadowViewCard_shadowOffsetX, dp2px(getContext(), DEFAULT_VALUE_SHADOW_OFFSET_X));
        shadowRadius = a.getInteger(R.styleable.ShadowViewCard_shadowRadius, DEFAULT_VALUE_SHADOW_RADIUS);
        a.recycle();
        setPadding(shadowLeftHeight, shadowTopHeight, shadowRightHeight, shadowBottomHeight);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
    }

    public static int dp2px(Context context, float dipValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }

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

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

    @Override
    protected void dispatchDraw(Canvas canvas) {
        Paint shadowPaint = new Paint();
        shadowPaint.setColor(Color.WHITE);
        shadowPaint.setStyle(Paint.Style.FILL);
        shadowPaint.setAntiAlias(true);
        float left = shadowLeftHeight;
        float top = shadowTopHeight;
        float right = getWidth() - shadowRightHeight;
        float bottom = getHeight() - shadowBottomHeight;
        shadowPaint.setShadowLayer(shadowRadius, shadowOffsetX, shadowOffsetX, shadowColor);
        RectF rectF = new RectF(left, top, right, bottom);
        canvas.drawRoundRect(rectF, shadowRound, shadowRound, shadowPaint);
        canvas.save();
        super.dispatchDraw(canvas);
    }
}
   <declare-styleable name="ShadowViewCard">
        <!--圆角度-->
        <attr name="shadowRound" format="dimension" />
        <!--阴影的高度-->
        <attr name="shadowLeftHeight" format="dimension" />
        <attr name="shadowTopHeight" format="dimension" />
        <attr name="shadowRightHeight" format="dimension" />
        <attr name="shadowBottomHeight" format="dimension" />
        <!--上方阴影的偏离度-->
        <attr name="shadowOffsetY" format="dimension" />
        <attr name="shadowOffsetX" format="dimension" />
        <!--阴影颜色-->
        <attr name="shadowCardColor" format="color" />
        <attr name="shadowColor" format="color" />
        <!--阴影颜色模糊度,越大越模糊-->
        <attr name="shadowRadius" format="integer" />
    </declare-styleable>
<color name="shadow_default_color">#1a000000</color>
 <color name="shadow_card_default_color">#ffffff</color>

原文地址:https://blog.51cto.com/14295695/2391946

时间: 2024-08-29 08:47:23

Android进阶:十二、最简单的方式实现自定义阴影效果的相关文章

Android 面试题总结之Android 进阶(二)

Android 之美 从0到1 之Android 进阶(二) 在上一章节中<Android 之美 从0到1 之Android 进阶(一)>中我们已经理解了一些View的基本知识并且知道如何自定义View.那么本章节将继续深入理解View,关于View的绘制流程,View的事件分发.刷新机制等等. 在阅读过程中有任何问题,请及时联系.如需转载请注明 fuchenxuan blog 本章系<Android 之美 从0到1 – 高手之路>Android 深入理解View的绘制流程. An

12. 蛤蟆的数据结构进阶十二排序实现之直接插入法

12. 蛤蟆的数据结构进阶十二排序实现之直接插入法 本篇名言:"路是脚踏出来的 ,历史是人写出来的,人的每一步行动都在书定自己的历史. --吉鸿昌" 接下来看下直接插入法的实现. 欢迎转载,转载请标明出处:http://blog.csdn.net/notbaron/article/details/47687631 1.  直接插入法 直接插入排序(straightinsertion sort) 每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序. 第一趟比较前两

Python进阶(十二)----re模块

Python进阶(十二)----re模块 一丶re模块 ? re模块是python将正则表达式封装之后的一个模块.正则表达式模式被编译成一系列的字节码,然后由用C编写的匹配引擎执行. #正则表达式: 从一串字符中,找出你想要的字符串. import re ### 单个元字符的匹配 # \W 除了数字 ,字母,中文, 下划线 print(re.findall('\W','dsadas1231 +1-+2*/,.')) # \w 匹配中文,数字,字母,下划线 print(re.findall('\w

Android之十二微信UI界面设计

Android之十二微信UI界面设计 corners_bg.xml <span style="font-size:14px;"><?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <solid android:color=&qu

Android入门(十二)SQLite事务、升级数据库

原文链接:http://www.orlion.ga/610/ 一.事务 SQLite支持事务,看一下Android如何使用事务:比如 Book表中的数据都已经很老了,现在准备全部废弃掉替换成新数据,可以先使用delete()方法将Book表中的数据删除, 然后再使用insert()方法将新的数据添加到表中.我们要保证的是,删除旧数据和添加新数据的操作必须一起完成,否则就还要继续保留原来的旧数据.                 Button replaceData = (Button) find

分布式进阶(十二)Docker给每一个启动的container指定一个ip, 这个如何做?

使用pipework工具. 前提:每个Container所做的工作现在还很少,可以不用save.commit. 为了便于通信,自定义一个网桥(192.168.1.180/24),使之IP与宿主主机IP在同一网段内. bridge模式 bridge模式是Docker默认的网络设置,此模式会为每一个容器分配Network Namespace.设置IP等,并将一个主机上的Docker容器连接到一个虚拟网桥上.下面着重介绍一下此模式. bridge模式的拓扑 当Docker server启动时,会在主机

Android学习十二:跑马灯程序实现(简单联系)

package org.tonny; import java.util.Timer; import java.util.TimerTask; import android.app.Activity; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import android.w

处女男学Android(十二)---Android 菜单(Menu)详解与应用

一.前言 转载请标明出处:http://blog.csdn.net/wlwlwlwl015/article/details/42170771 本篇blog将记录一下Android中菜单的使用方法,虽然在新版本中推荐使用ActionBar去替代菜单,但我认为菜单依然是挺不错的一种UI交互组件,比ActionBar好点一些,对于手指不灵活的人比起来ActionBar右上角的那个方块小按钮,或许ta会更喜欢Menu呢,废话不多说,下面就具体介绍一下Android中关于Menu的使用方法. 二.创建第一

Android进阶——构建UI布局的多种方式总结

引言 作为Android App,给人第一印象的就是用户界面(UI),简洁友好的UI,自然会给用户优秀的体验,自然很容易就得到用户的认可和赞许,这样App才变得真正的有价值.所以作为开发App的第一步,UI尤为重要,构建UI有很多种方式:xml静态布局.java动态代码.HTML构建(借助WebView)和第三方开源框架等. 一.构成UI的基本元素--View和ViewGroup概述 在Android中绝大部分的UI组件都是存放在android.widget包及其子包.android.view包