转载请注明出处:王亟亟的大牛之路html
上一篇大体介绍了Material Design的一些基本概念传送门:http://blog.csdn.net/ddwhan0123/article/details/50541561java
这一片来详细学习下里面的内容,这篇分为两部分一部分是原理分析,一部分是代码分析。android
先简要的介绍一些理论知识,顺便温顾下基础知识git
button由文字和/或图标组成,文字及图标必须能让人轻易地和点击后展现的内容联系起来。
基本的button有三种:github
颜色饱满的图标应当是功能性的,尽可能避免把他们做为纯粹装饰用的元素。算法
在咱们AS建包以后就会有一种Blank Activity的模式,里面会有一个悬浮响应按钮(Floating action button)canvas
跟咱们一直搜的什么360悬浮button一个实现吧,但是要点明一点中心。他是有厚度的。ide
大体像这样:函数
咱们的控件都是有厚度的。他们不在一个层级上。形成了层次感。工具
顺便上一下咱们的案例对象的效果:
从图中咱们可以看出,button的事件是有响应的(也就是你们一直搜的“地震”传播的效果)
案例中有一个问题。就是颜色的不统一。
注意事项:
button的设计应当和应用的颜色主题保持一致。(这一点仍是很是重要的,尽可能不要用户认为一种杂乱感)
设计的过程当中必定不要让咱们的全副button重叠在底部的BAR/Button上。即便他们不是统一厚度
再提一下button的种类(突出注意下他们的使用场景):
悬浮响应button是促进动做里的特殊类型。 是一个圆形的漂浮在界面之上的、拥有一系列特殊动做的button,这些动做一般和变换、启动、以及它自己的转换锚点相关。
浮动button使button在比較拥挤的界面上更清晰可见。能给大多数扁平的布局带来层次感。
假设需要一个对用户持续可见的功能button,应该首先考虑使用悬浮响应button。假设需要一个非主要、但是能高速定位到的button,则可以使用底部固定button。
扁平button通常用于对话框或者工具栏。 可避免页面上过多无心义的层叠。
对话框中使用扁平button做为主要button类型以免过多的层次叠加。
button类型应该基于主button、屏幕上容器的数量以及整体布局来进行选择。
首先。审视一遍你的button功能: 它是否是很重要而且应用普遍到需要用上悬浮响应button?
而后,基于放置button的容器以及屏幕上层次堆叠的数量来选择使用浮动button仍是扁平button。而且应该避免过多的层叠。
最后,检查你的布局。
一个容器应该仅仅使用一种类型的button。
仅仅在比較特殊的状况下(比方需要强调一个浮起的效果)才应该混合使用多种类型的button。
不少其它内容可以看原文:http://www.google.com/design/spec/components/buttons.html
接下来咱们来分析下咱们的实现效果--ButtonFlat和ButtonRectangle
在这里再次感谢开源做者:https://github.com/navasmdc/MaterialDesignLibrary
先说下ButtonRectangle
public class ButtonRectangle extends Button
public abstract class Button extends CustomView
public class CustomView extends RelativeLayout
final static String MATERIALDESIGNXML = "http://schemas.android.com/apk/res-auto"; final static String ANDROIDXML = "http://schemas.android.com/apk/res/android"; final int disabledBackgroundColor = Color.parseColor("#E2E2E2"); int beforeBackground; // Indicate if user touched this view the last time public boolean isLastTouch = false; public CustomView(Context context, AttributeSet attrs) { super(context, attrs); }
<span style="white-space:pre"> </span>@Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); if(enabled) setBackgroundColor(beforeBackground); else setBackgroundColor(disabledBackgroundColor); invalidate(); }
<span style="white-space:pre"> </span>boolean animation = false; @Override protected void onAnimationStart() { super.onAnimationStart(); animation = true; } @Override protected void onAnimationEnd() { super.onAnimationEnd(); animation = false; }
<span style="white-space:pre"> </span>@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if(animation) invalidate(); }
50-55刷新View
咱们可以很是清楚的看出,CustomView是个工具类。并未发现咱们想要的实现,咱们继续看下去。
接下来。咱们来到了Button
<span style="white-space:pre"> </span>int minWidth; int minHeight; int background; float rippleSpeed = 12f; int rippleSize = 3; Integer rippleColor; OnClickListener onClickListener; boolean clickAfterRipple = true; int backgroundColor = Color.parseColor("#1E88E5"); TextView textButton;
24-33一系列的參数声明。 textButton就是咱们等会要出现的字。还有就是监听事件,以及一些位置的參数
public Button(Context context, AttributeSet attrs) { super(context, attrs); setDefaultProperties(); clickAfterRipple = attrs.getAttributeBooleanValue(MATERIALDESIGNXML, "animate", true); setAttributes(attrs); beforeBackground = backgroundColor; if (rippleColor == null) rippleColor = makePressColor(); } protected void setDefaultProperties() { // Min size setMinimumHeight(Utils.dpToPx(minHeight, getResources())); setMinimumWidth(Utils.dpToPx(minWidth, getResources())); // Background shape setBackgroundResource(background); setBackgroundColor(backgroundColor); }
// Set atributtes of XML to View abstract protected void setAttributes(AttributeSet attrs); // ### RIPPLE EFFECT ### float x = -1, y = -1; float radius = -1;
@Override public boolean onTouchEvent(MotionEvent event) { invalidate(); if (isEnabled()) { isLastTouch = true; if (event.getAction() == MotionEvent.ACTION_DOWN) { radius = getHeight() / rippleSize; x = event.getX(); y = event.getY(); } else if (event.getAction() == MotionEvent.ACTION_MOVE) { radius = getHeight() / rippleSize; x = event.getX(); y = event.getY(); if (!((event.getX() <= getWidth() && event.getX() >= 0) && (event .getY() <= getHeight() && event.getY() >= 0))) { isLastTouch = false; x = -1; y = -1; } } else if (event.getAction() == MotionEvent.ACTION_UP) { if ((event.getX() <= getWidth() && event.getX() >= 0) && (event.getY() <= getHeight() && event.getY() >= 0)) { radius++; if (!clickAfterRipple && onClickListener != null) { onClickListener.onClick(this); } } else { isLastTouch = false; x = -1; y = -1; } } else if (event.getAction() == MotionEvent.ACTION_CANCEL) { isLastTouch = false; x = -1; y = -1; } } return true; }
65行,这个onTouchEvent方法的整个过程当中,UI会被不断的刷新。
66,推断屏幕是否可见
67行,当屏幕被View自己被触摸后父类的isLastTouch为true(咱们看看他究竟干嘛用)
68-72行,当手直接出屏幕,初始化x,y为X,Y的手指坐标。制定“圆圈”半径。
72-82行。假设 手指的移动范围超出了空间的区域isLastTouch为false,X,Y坐标置为-1
82-94行,假设手指的触控点还在空间范围内的状况下手指离开屏幕咱们的圆自增摆脱无效值并且触发Click事件,反之如上,都初始化一圈。
94-98行,用于操做当用户保持按下操做,并从空间区域移到其它外层控件时触发(幻想下滑动listview的行为就理解了。为何划得时候。离开的时候都没有触发OnItemClick)
一整个onTouchEvent都是对绘制内容前參数的收集以及初始化,咱们继续读源代码
<span style="white-space:pre"> </span>@Override protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { if (!gainFocus) { x = -1; y = -1; } }
<span style="white-space:pre"> </span>@Override public boolean onInterceptTouchEvent(MotionEvent ev) { // super.onInterceptTouchEvent(ev); return true; }
public Bitmap makeCircle() { Bitmap output = Bitmap.createBitmap( getWidth() - Utils.dpToPx(6, getResources()), getHeight() - Utils.dpToPx(7, getResources()), Config.ARGB_8888); Canvas canvas = new Canvas(output); canvas.drawARGB(0, 0, 0, 0); Paint paint = new Paint(); paint.setAntiAlias(true); paint.setColor(rippleColor); canvas.drawCircle(x, y, radius, paint); if (radius > getHeight() / rippleSize) radius += rippleSpeed; if (radius >= getWidth()) { x = -1; y = -1; radius = getHeight() / rippleSize; if (onClickListener != null && clickAfterRipple) onClickListener.onClick(this); } return output; }
首先画了个Bitmap做为底板,填充颜色。固定圆圈的大小。直至大到超出控件大小被初始化继续维持事件触发
<span style="white-space:pre"> </span>protected int makePressColor() { int r = (this.backgroundColor >> 16) & 0xFF; int g = (this.backgroundColor >> 8) & 0xFF; int b = (this.backgroundColor >> 0) & 0xFF; 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); }
@Override public void setOnClickListener(OnClickListener l) { onClickListener = l; }
public void setBackgroundColor(int color) { this.backgroundColor = color; if (isEnabled()) beforeBackground = backgroundColor; try { LayerDrawable layer = (LayerDrawable) getBackground(); GradientDrawable shape = (GradientDrawable) layer .findDrawableByLayerId(R.id.shape_bacground); shape.setColor(backgroundColor); rippleColor = makePressColor(); } catch (Exception ex) { // Without bacground } }
再如下就是一系列的set方法了,咱们来分析下刚才那一系列的实现
首先,咱们的圈必须是在手指触发事件以后再绘制。并且离开空间范围内的触发是无效的不会触发动画效果。仅仅有手指的触摸圆圈,这个圆圈的大小取决于getHeight/规定值的算法。规定值咱们可以设置。这个父类构建了咱们button所需的动画和计算的基础。
最后咱们来讲下ButtonRectangle
<span style="white-space:pre"> </span>TextView textButton; int paddingTop,paddingBottom, paddingLeft, paddingRight; public ButtonRectangle(Context context, AttributeSet attrs) { super(context, attrs); setDefaultProperties(); } @Override protected void setDefaultProperties(){ // paddingBottom = Utils.dpToPx(16, getResources()); // paddingLeft = Utils.dpToPx(16, getResources()); // paddingRight = Utils.dpToPx(16, getResources()); // paddingTop = Utils.dpToPx(16, getResources()); super.minWidth = 80; super.minHeight = 36; super.background = R.drawable.background_button_rectangle; super.setDefaultProperties(); }
18-37行,初始化咱们的控件。
// Set atributtes of XML to View protected void setAttributes(AttributeSet attrs){ //Set background Color // Color by resource int bacgroundColor = attrs.getAttributeResourceValue(ANDROIDXML,"background",-1); if(bacgroundColor != -1){ setBackgroundColor(getResources().getColor(bacgroundColor)); }else{ // Color by hexadecimal // Color by hexadecimal background = attrs.getAttributeIntValue(ANDROIDXML, "background", -1); if (background != -1) setBackgroundColor(background); } // Set Padding String value = attrs.getAttributeValue(ANDROIDXML,"padding"); // if(value != null){ // float padding = Float.parseFloat(value.replace("dip", "")); // paddingBottom = Utils.dpToPx(padding, getResources()); // paddingLeft = Utils.dpToPx(padding, getResources()); // paddingRight = Utils.dpToPx(padding, getResources()); // paddingTop = Utils.dpToPx(padding, getResources()); // }else{ // value = attrs.getAttributeValue(ANDROIDXML,"paddingLeft"); // paddingLeft = (value == null) ? paddingLeft : (int) Float.parseFloat(value.replace("dip", "")); // value = attrs.getAttributeValue(ANDROIDXML,"paddingTop"); // paddingTop = (value == null) ? paddingTop : (int) Float.parseFloat(value.replace("dip", "")); // value = attrs.getAttributeValue(ANDROIDXML,"paddingRight"); // paddingRight = (value == null) ? paddingRight : (int) Float.parseFloat(value.replace("dip", "")); // value = attrs.getAttributeValue(ANDROIDXML,"paddingBottom"); // paddingBottom = (value == null) ?paddingBottom : (int) Float.parseFloat(value.replace("dip", "")); // } // Set text button String text = null; int textResource = attrs.getAttributeResourceValue(ANDROIDXML,"text",-1); if(textResource != -1){ text = getResources().getString(textResource); }else{ text = attrs.getAttributeValue(ANDROIDXML,"text"); } if(text != null){ textButton = new TextView(getContext()); textButton.setText(text); textButton.setTextColor(Color.WHITE); textButton.setTypeface(null, Typeface.BOLD); RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT); params.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE); params.setMargins(Utils.dpToPx(5, getResources()), Utils.dpToPx(5, getResources()), Utils.dpToPx(5, getResources()), Utils.dpToPx(5, getResources())); textButton.setLayoutParams(params); addView(textButton); // FrameLayout.LayoutParams params = (LayoutParams) textView.getLayoutParams(); // params.width = getWidth(); // params.gravity = Gravity.CENTER_HORIZONTAL; //// params.setMargins(paddingLeft, paddingTop, paddingRight, paddingRight); // textView.setLayoutParams(params);textColor int textColor = attrs.getAttributeResourceValue(ANDROIDXML,"textColor",-1); if(textColor != -1){ textButton.setTextColor(textColor); }else{ // Color by hexadecimal // Color by hexadecimal textColor = attrs.getAttributeIntValue(ANDROIDXML, "textColor", -1); if (textColor != -1) textButton.setTextColor(textColor); } int[] array = {android.R.attr.textSize}; TypedArray values = getContext().obtainStyledAttributes(attrs, array); float textSize = values.getDimension(0, -1); values.recycle(); if(textSize != -1) textButton.setTextSize(textSize); } rippleSpeed = attrs.getAttributeFloatValue(MATERIALDESIGNXML, "rippleSpeed", Utils.dpToPx(6, getResources())); }
Integer height; Integer width; @Override protected void onDraw(Canvas canvas) { // if(!txtCenter) // centrarTexto(); super.onDraw(canvas); if (x != -1) { Rect src = new Rect(0, 0, getWidth()-Utils.dpToPx(6, getResources()), getHeight()-Utils.dpToPx(7, getResources())); Rect dst = new Rect(Utils.dpToPx(6, getResources()), Utils.dpToPx(6, getResources()), getWidth()-Utils.dpToPx(6, getResources()), getHeight()-Utils.dpToPx(7, getResources())); canvas.drawBitmap(makeCircle(), src, dst, null); invalidate(); } }
推断是否有接触,假设没有就不绘制(做者拿X做为比較,事实上X Y都同样。不为-1就是接触了,没接触(或接触区域不对)的时候均为-1)
接着画了2个方块和咱们以前计算的圆圈作组合效果。
总结:
实现。事实上并不是太难。关键是需要更好的思考怎么实现更好。这里大体的再解释下流程。
首先,咱们有一个大的方块他是RelativeLayout。
而后咱们中间用OnTouchEvent来实现用户的触控过程和触控监听。
不断的绘制用户移动的触控点的圆。
当用户以合理的方式。在符合逻辑的位置up手指的时候出发画布的涟漪效果,这边使用色差和2个方块+圆变大过程的效果来呈现的。
过程当中可能有我语言表达的问题。欢迎你们提出。