剖析项目名称: 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
- Buttons
- 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);
}
}