深度剖析之 Material Design Android Library

本文详细剖析了MaterialDesignAndroidLibrary开源项目的实现细节,包括按钮、开关、进度条等组件的源码分析,以及如何正确导入和解决冲突问题。通过实例演示,帮助开发者深入理解Android组件的自定义实现和事件处理逻辑。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

剖析项目名称: Material Design Android Library
剖析原项目地址:https://github.com/navasmdc/MaterialDesignLibrary
剖析理由:只知其然而不知其所以然,如此不好。想要快速的进阶,不走寻常路,剖析开源项目,深入理解扩展知识,仅仅这样还不够,还需要如此:左手爱哥的设计模式,右手重构改善既有设计,如此漫长打坐,回过头再看来时的路,书已成山,相信翔哥说的,量变引起质变。,


  • How to use
  • Components
    • Buttons
      • Flat Button
      • Rectangle Button
      • Float Button
      • Float small button
    • switches
      • CheckBox
      • Switch
    • progress
      • Progress bar circular circular indeterminate
      • Progress bar indeterminate
      • Progress bar indeterminate determinater
      • Progress bar determinate
      • Slider
      • Slider with number indicator
  • widgets
    • SnackBar
    • Dialog
    • Color Selector

如果你想要使用这个库,您只需要下载MaterialDesign项目,将其导入到您的工作区,并添加这个项目作为一个依赖,导入方法(Android Studio 编辑器):

repositories {
    jcenter()
}

dependencies {
    compile 'com.github.navasmdc:MaterialDesign:1.5@aar'
}

比较早的Android Studio版本导入该库是没问题的,但是随着时间的变化,Gradle自动comple了 ‘com.android.support:design:22.2.1’该库,该库和我们将要导入的库有自定义属性rippleColor属性重复定义冲突,摆在我们面前的有两个办法,一试把Material Design Android Library下载下来,修改该属性名字和关联类,另一种就是更换库(该库兼容到16):

repositories {
    maven { url "https://jitpack.io" }
}

compile 'com.github.vajro:MaterialDesignLibrary:1.6'

该库自定义控件包含自定义属性,如果你在xml里面需要使用到它们,需要导入下面这句话(materialdesign根据自己喜好修改,这句话是属于自定义属性相关知识的内容,如果不了解请参考官方资料):

  xmlns:materialdesign="http://schemas.android.com/apk/res-auto"

如果你要使用滚动视图,建议您使用这个库中提供的ScrollView来避免自定义组件的问题。使用这个组件方法:

<com.gc.materialdesign.views.ScrollView 
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:materialdesign="http://schemas.android.com/apk/res-auto"
   android:id="@+id/scroll"
   android:layout_width="match_parent"
   android:layout_height="match_parent">
</com.gc.materialdesign.views.ScrollView>

先来一睹为快吧,看一下运行效果图(模拟器录制有点卡,换了游视秀app录制的.mp4,再通过桌面录屏工具录制播放的.mp4成gif,如果你感兴趣可以试试):

Button控件提供了ButtonFlat、ButtonRectangle、ButtonFloat、ButtonFloatSmall四个控件,下面我们逐步分析它。这几个类都继承自定义的抽象类Button,而Button继承自CustomView.

那么先来看CustomView类

public class CustomView extends RelativeLayout {

    /**不能点击的背景色*/ 
    final int disabledBackgroundColor = Color.parseColor("#E2E2E2");

    /**能点击的背景色*/ 
    int beforeBackground;

    /**自定义ScrollView用于判断是否拦截Touch事件*/ 
    public boolean isLastTouch = false;

    /**是否在执行动画,根据这个参数选择重绘*/
    boolean animation = false;

   public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
   }

   /**
    * 设置是否可点击,调用View的setEnable设置,与此同时,根据enabled参数,选择设置背景颜色
    **/ 
   public void setEnabled(boolean enabled) {
        super.setEnabled(enabled);
        if(enabled) {
            this.setBackgroundColor(this.beforeBackground);
        } else {
            this.setBackgroundColor(this.disabledBackgroundColor);
        }
        //调用重绘  
        this.invalidate();
    }

   /**
    * 重写RelativeLayout继承自ViewGroup的动画启动方法,并对animation重写赋值为true
    **/ 
    protected void onAnimationStart() {
        super.onAnimationStart();
        this.animation = true;
    }

   /**
    * 重写RelativeLayout继承自ViewGroup的动画结束方法,并对animation重写赋值为false
    **/ 
    protected void onAnimationEnd() {
        super.onAnimationEnd();
        this.animation = false;
    }

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(this.animation) {
            //如果动画是启动状态,就调用重绘自身
            this.invalidate();
        }
    }
}

抽象Button类

public abstract class Button extends CustomView {

  static final String ANDROIDXML = "http://schemas.android.com/apk/res/android";
    /**最小宽度*/
    int minWidth;
    /**最小高度*/
    int minHeight;
    /**背景*/
    int background;
    /**波纹绘制速度*/
    float rippleSpeed = 12.0F;
    /**波纹大小*/
    int rippleSize = 3;
    /**波纹颜色*/
    Integer rippleColor;
    /**点击事件*/
    OnClickListener onClickListener;
    /**是否有点击之后的波纹*/
    boolean clickAfterRipple = true;
    /**背景颜色*/
    int backgroundColor = Color.parseColor("#1E88E5");
    /**文字控件TextView*/
    TextView textButton;
    /**X坐标*/
    float x = -1.0F;
    /**Y坐标*/
    float y = -1.0F;
    /**绘制的范围*/
    float radius = -1.0F;

    public Button(Context context, AttributeSet attrs) {
        super(context, attrs);
        //设置默认值
        this.setDefaultProperties();
        //获取属性,点击后是否有波纹效果,默认有
        this.clickAfterRipple = attrs.getAttributeBooleanValue("http://schemas.android.com/apk/res-auto", "animate", true);
        //空实现方法,供子类使用
        this.setAttributes(attrs);
        this.beforeBackground = this.backgroundColor;
        if(this.rippleColor == null) {
            //波纹颜色
            this.rippleColor = Integer.valueOf(this.makePressColor());
        }

    }
}

makePressColor()方法获取的是波纹颜色:

 protected int makePressColor() {
        //backgroundColor 颜色值通过位移与225(0-225代表的是颜色值的深度,数值越大,颜色越深,RGB是红绿蓝三色对应的数值组合,
        //这里通过位移获取到RGB各自对应的色值),再把RGB对应的数值降低30,最后通过Color提供的RGB方法重新生成颜色即rippleColor.
        int r = this.backgroundColor >> 16 & 255;
        int g = this.backgroundColor >> 8 & 255;
        int b = this.backgroundColor >> 0 & 255;
        r = r - 30 < 0?0:r - 30;
        g = g - 30 < 0?0:g - 30;
        b = b - 30 < 0?0:b - 30;
        return Color.rgb(r, g, b);
    }

CustomView 继承自RelativeLayout,所以拥有事件分发拦截方法onInterceptTouchEvent,这里通过拦截事件,交给 onTouchEvent(MotionEvent event)处理。

  public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
  }

   public boolean onTouchEvent(MotionEvent event) {
        this.invalidate();
        if(this.isEnabled()) {
            //允许点击,isLastTouch赋值为true,自定义ScrollView了通过该参数控制是否出Touch事件
            this.isLastTouch = true;
            if(event.getAction() == 0) {

    //action参照如下定义
    /**
     * Constant for {@link #getActionMasked}: A pressed gesture has started, the
     * motion contains the initial starting location.
     * <p>
     * This is also a good time to check the button state to distinguish
     * secondary and tertiary button clicks and handle them appropriately.
     * Use {@link #getButtonState} to retrieve the button state.
     * </p>
     */
   // public static final int ACTION_DOWN             = 0;

    /**
     * Constant for {@link #getActionMasked}: A pressed gesture has finished, the
     * motion contains the final release location as well as any intermediate
     * points since the last down or move event.
     */
   // public static final int ACTION_UP               = 1;

    /**
     * Constant for {@link #getActionMasked}: A change has happened during a
     * press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}).
     * The motion contains the most recent point, as well as any intermediate
     * points since the last down or move event.
     */
   // public static final int ACTION_MOVE             = 2;
    /**
     * Constant for {@link #getActionMasked}: The current gesture has been aborted.
     * You will not receive any more points in it.  You should treat this as
     * an up event, but not perform any action that you normally would.
     */
    //public static final int ACTION_CANCEL           = 3;

                 //按下时计算波纹的size,并记录坐标
                this.radius = (float)(this.getHeight() / this.rippleSize);
                this.x = event.getX();
                this.y = event.getY();
            } else if(event.getAction() == 2) {
                //移动时计算波纹的size,并记录坐标
                this.radius = (float)(this.getHeight() / this.rippleSize);
                this.x = event.getX();
                this.y = event.getY();
                //判断移动中的坐标还是否在当前视图范围内
                if(event.getX() > (float)this.getWidth() || event.getX() < 0.0F || event.getY() > (float)this.getHeight() || event.getY() < 0.0F) {
                    this.isLastTouch = false;
                    this.x = -1.0F;
                    this.y = -1.0F;
                }
            } 
            //手抬起时,计算相关变量值,并判断是否回调onclick函数
            else if(event.getAction() == 1) {
                if(event.getX() <= (float)this.getWidth() && event.getX() >= 0.0F && event.getY() <= (float)this.getHeight() && event.getY() >= 0.0F) {
                    ++this.radius;
                    if(!this.clickAfterRipple && this.onClickListener != null) {
                        this.onClickListener.onClick(this);
                    }
                } else {
                    this.isLastTouch = false;
                    this.x = -1.0F;
                    this.y = -1.0F;
                }
            } else if(event.getAction() == 3) {
                //取消绘制
                this.isLastTouch = false;
                this.x = -1.0F;
                this.y = -1.0F;
            }
        }

        return true;
    }

内部提供一个绘图方法,创一个大小的圆形图,并绘制到画布,返回创建的Bitmap,并绑定监听

 public Bitmap makeCircle() {
        Bitmap output = Bitmap.createBitmap(this.getWidth() - Utils.dpToPx(6.0F, this.getResources()), this.getHeight() - Utils.dpToPx(7.0F, this.getResources()), Config.ARGB_8888);
        Canvas canvas = new Canvas(output);
        canvas.drawARGB(0, 0, 0, 0);
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(this.rippleColor.intValue());
        canvas.drawCircle(this.x, this.y, this.radius, paint);
        if(this.radius > (float)(this.getHeight() / this.rippleSize)) {
            this.radius += this.rippleSpeed;
        }

        if(this.radius >= (float)this.getWidth()) {
            this.x = -1.0F;
            this.y = -1.0F;
            this.radius = (float)(this.getHeight() / this.rippleSize);
            if(this.onClickListener != null && this.clickAfterRipple) {
                this.onClickListener.onClick(this);
            }
        }

        return output;
    }

现在开始正真分析这几个控件了,ButtonFlat:

public class ButtonFlat extends Button {

    protected void setAttributes(AttributeSet attrs) {
        String text = null;
        //获取android定义的属性android:text=""
        int textResource = attrs.getAttributeResourceValue("http://schemas.android.com/apk/res/android", "text", -1);
        if(textResource != -1) {
            text = this.getResources().getString(textResource);
        } else {
            text = attrs.getAttributeValue("http://schemas.android.com/apk/res/android", "text");
        }

        if(text != null) {
            //如果有text值,则创建一个TextView,并赋值
            this.textButton = new TextView(this.getContext());
            this.textButton.setText(text.toUpperCase());
            this.textButton.setTextColor(this.backgroundColor);
            this.textButton.setTypeface((Typeface)null, 1);
            LayoutParams bacgroundColor = new LayoutParams(-2, -2);
            bacgroundColor.addRule(13, -1);
            this.textButton.setLayoutParams(bacgroundColor);
            //把这个textView添加到Layout里面作为子View
            this.addView(this.textButton);
        }

        //获取背景属性
        int bacgroundColor1 = attrs.getAttributeResourceValue("http://schemas.android.com/apk/res/android", "background", -1);
        if(bacgroundColor1 != -1) {
            this.setBackgroundColor(this.getResources().getColor(bacgroundColor1));
        } else {
            this.background = attrs.getAttributeIntValue("http://schemas.android.com/apk/res/android", "background", -1);
            if(this.background != -1) {
                this.setBackgroundColor(this.background);
            }
        }

    }
}

   protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(this.x != -1.0F) {
            Paint paint = new Paint();
            paint.setAntiAlias(true);
            paint.setColor(this.makePressColor());
            //根据按下坐标画圆
            canvas.drawCircle(this.x, this.y, this.radius, paint);
            if(this.radius > (float)(this.getHeight() / this.rippleSize)) {
                this.radius += this.rippleSpeed;
            }

            if(this.radius >= (float)this.getWidth()) {
                //当波纹的大小超出了视图范围,就不在绘制并通过OnClickListener回调到主线程处理相关事件
                this.x = -1.0F;
                this.y = -1.0F;
                this.radius = (float)(this.getHeight() / this.rippleSize);
                if(this.onClickListener != null && this.clickAfterRipple) {
                    this.onClickListener.onClick(this);
                }
            }
            //再调重绘绘制自身(绘制范围还没超出限定范围还会继续继续绘制根据radius大小的改变)
            this.invalidate();
        }

      }

ButtonFlot相比较ButtonFlat而言,把内部创建的TextView替换成了ImageView,改变不大,在绘制时onDraw方法里面drawBitmap(…)调用了cropCircle和makeCircle方法得到波纹bitmap:

 public Bitmap cropCircle(Bitmap bitmap) {

        //..............此处略...............

        canvas.drawCircle((float)(bitmap.getWidth() / 2), (float)(bitmap.getHeight() / 2), (float)(bitmap.getWidth() / 2), paint);
        //不同的模式呈现结果是不同的,这里用的SRC_IN:取两层绘制交集。显示上层。
        paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
        canvas.drawBitmap(bitmap, rect, rect, paint);
        return output;
    }

其他几个Button的自定义暂时就不提了,基本实现都差不多,上面效果图里面有个底部Button弹出效果,在xml布局如下(通过自定义属性传入动画状态和图标资源):

 <com.gc.materialdesign.views.ButtonFloat
                android:id="@+id/buttonFloat"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:layout_alignParentBottom="true"
                android:layout_marginRight="24dp"
                android:background="#1E88E5"
                materialdesign:animate="true"
                materialdesign:iconDrawable="@drawable/ic_action_new" />

在ButtonFloat里面通过实现父类的空方法里,获取了这两个属性,同时执行相应的动画:

  protected void setAttributes(AttributeSet attrs) {
        //..........略..........
        iconResource = attrs.getAttributeResourceValue("http://schemas.android.com/apk/res-auto", "iconDrawable", -1);
        if(iconResource != -1) {
            this.drawableIcon = this.getResources().getDrawable(iconResource);
        }

        final boolean animate = attrs.getAttributeBooleanValue("http://schemas.android.com/apk/res-auto", "animate", false);
        this.post(new Runnable() {
            public void run() {
                ButtonFloat.this.showPosition = ViewHelper.getY(ButtonFloat.this) - (float)Utils.dpToPx(24.0F, ButtonFloat.this.getResources());
                ButtonFloat.this.hidePosition = ViewHelper.getY(ButtonFloat.this) + (float)(ButtonFloat.this.getHeight() * 3);
                if(animate) {
                    ViewHelper.setY(ButtonFloat.this, ButtonFloat.this.hidePosition);
                    ButtonFloat.this.show();
                }

            }
        });
    }

动画显示和隐藏的控制方法(原理:通过插值器和Y值变化控制):

  public void show() {
        ObjectAnimator animator = ObjectAnimator.ofFloat(this, "y", new float[]{this.showPosition});
        animator.setInterpolator(new BounceInterpolator());
        animator.setDuration(1500L);
        animator.start();
        this.isShow = true;
    }

    public void hide() {
        ObjectAnimator animator = ObjectAnimator.ofFloat(this, "y", new float[]{this.hidePosition});
        animator.setInterpolator(new BounceInterpolator());
        animator.setDuration(1500L);
        animator.start();
        this.isShow = false;
    }

Button自定义暂时告一段落,接着来看Switchs相关控件的自定义。

  <com.gc.materialdesign.views.CheckBox
                android:id="@+id/checkBox"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:background="#1E88E5"
                materialdesign:check="true" />

checkBox按下效果有个圆形出现,而颜色值是根据isChecked改变的,实现方法在onTouch的down up 通过调用重绘,onDraw里绘制出图像

  public void invalidate() {
        //重绘根据选中状态,改变选中图片
        this.checkView.invalidate();
        super.invalidate();
    }
  protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(this.press) {
            Paint paint = new Paint();
            paint.setAntiAlias(true);
            paint.setColor(this.check?this.makePressColor():Color.parseColor("#446D6D6D"));
            canvas.drawCircle((float)(this.getWidth() / 2), (float)(this.getHeight() / 2), (float)(this.getWidth() / 2), paint);
            this.invalidate();
        }

    }

checkView是CheckBox类里的内部类Check,继承自View类:

 class Check extends View {
 public void changeBackground() {
            if(CheckBox.this.check) {//根据不同的状态选择不同的图片
                this.setBackgroundResource(drawable.background_checkbox_check);
                LayerDrawable layer = (LayerDrawable)this.getBackground();
                GradientDrawable shape = (GradientDrawable)layer.findDrawableByLayerId(id.shape_bacground);
                shape.setColor(CheckBox.this.backgroundColor);
            } else {
                this.setBackgroundResource(drawable.background_checkbox_uncheck);
            }

        }

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

            //...............此处略................... 

            Rect src = new Rect(40 * CheckBox.this.step, 0, 40 * CheckBox.this.step + 40, 40);
            Rect dst = new Rect(0, 0, this.getWidth() - 2, this.getHeight());
            canvas.drawBitmap(this.sprite, src, dst, (Paint)null);
        }
    }
 }

com.gc.materialdesign.views.Switch控件就不贴代码了简单说一下原理,onTouch时判断滑动的距离是否大于某个固定值,从而改变状态,调用回调函数,在onDraw里面重绘改变界面。这里还借助了第三方开源动画库,调用 ViewHelper.setX()方法。关于进度自定义控件ProgressBarCircularIndeterminate实现原理:先绘制一个圆动态改变半径,然后在绘制一个圆通个xfermode最后得到一个周长弧,累赘代码就不贴了,通个onDraw方法我学到了一点知识:绘制内容抽离到方法,通个句柄选择绘制,再通过调用自身再次绘制未执行部分

  protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(!this.firstAnimationOver) {
            this.drawFirstAnimation(canvas);
        }

        if(this.cont > 0) {
            this.drawSecondAnimation(canvas);
        }

        this.invalidate();
    }

Slider类的自定义的实现原理,在setAttributes根据showNumberIndicator为true时,选择创建弹出浮动在上方的圆和值的dialog,onTouch是判断选择修改dialog的隐藏、显示、位置和值。代码就不贴了。SnackBar继承自Dialog,没啥好说的,就onCreate的view布局,引入基本配置,onDismis 、show方法执行动画,此处略过了。ColorSelector也是一个Dialog没啥好说的,三个Slider控件的值组合成Color从而改变颜色,通个监听及时回调,这里也略过了。

之前提到的滚动视图建议用该库自定义的ScrollView(判断了子类是否是CustomView,根据isLastTouch属性判断是否拦截Touch事件)


public class ScrollView extends android.widget.ScrollView {
    public ScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public boolean onTouchEvent(MotionEvent ev) {
        for(int i = 0; i < ((ViewGroup)this.getChildAt(0)).getChildCount(); ++i) {
            try {
                CustomView e = (CustomView)((ViewGroup)this.getChildAt(0)).getChildAt(i);
                if(e.isLastTouch) {
                    e.onTouchEvent(ev);
                    return true;
                }
            } catch (ClassCastException var4) {
                ;
            }
        }

        return super.onTouchEvent(ev);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值