Material Design学习之 Button(详细分析,传说中的水滴动画)

转载请注明出处:王亟亟的大牛之路

      上一篇大致介绍了Material Design的一些基本概念传送门:http://blog.csdn.net/ddwhan0123/article/details/50541561

这一片来具体学习下里面的内容,这篇分为两部分一部分是原理分析,一部分是代码分析。

先简要的介绍一些理论知识,顺便温顾下基础知识

按钮

按钮由文字和/或图标组成,文字及图标必须能让人轻易地和点击后展示的内容联系起来。
主要的按钮有三种:

  • 悬浮响应按钮(Floating action button), 点击后会产生墨水扩散效果的圆形按钮。
  • 浮动按钮(Raised button), 常见的方形纸片按钮,点击后会产生墨水扩散效果。
  • 扁平按钮(Flat button), 点击后产生墨水扩散效果,和浮动按钮的区别是没有浮起的效果。

颜色饱满的图标应当是功能性的,尽量避免把他们作为纯粹装饰用的元素。

在我们AS建包之后就会有一种Blank Activity的模式,里面会有一个悬浮响应按钮(Floating action button)

跟我们一直搜的什么360悬浮按钮一个实现吧,但是要点明一点中心,他是有厚度的。


大致像这样:



我们的控件都是有厚度的,他们不在一个层级上,造成了层次感。

顺便上一下我们的案例对象的效果:

从图中我们可以看出,按钮的事件是有响应的(也就是大家一直搜的“地震”传播的效果)

案例中有一个问题,就是颜色的不统一。

注意事项:

按钮的设计应当和应用的颜色主题保持一致。(这一点还是很重要的,尽量不要用户觉得一种杂乱感)

设计的过程中一定不要让我们的全副按钮重叠在底部的BAR/Button上,即使他们不是统一厚度

再提一下按钮的种类(突出注意下他们的使用场景):

悬浮响应按钮

悬浮响应按钮是促进动作里的特殊类型。 是一个圆形的漂浮在界面之上的、拥有一系列特殊动作的按钮,这些动作通常和变换、启动、以及它本身的转换锚点相关。

浮动按钮

浮动按钮使按钮在比较拥挤的界面上更清晰可见。能给大多数扁平的布局带来层次感。

底部固定按钮

如果需要一个对用户持续可见的功能按钮,应该首先考虑使用悬浮响应按钮。如果需要一个非主要、但是能快速定位到的按钮,则可以使用底部固定按钮。

扁平按钮

扁平按钮一般用于对话框或者工具栏, 可避免页面上过多无意义的层叠。

对话框中的按钮

对话框中使用扁平按钮作为主要按钮类型以避免过多的层次叠加。

主按钮

按钮类型应该基于主按钮、屏幕上容器的数量以及整体布局来进行选择。

首先,审视一遍你的按钮功能: 它是不是非常重要而且应用广泛到需要用上悬浮响应按钮?

然后,基于放置按钮的容器以及屏幕上层次堆叠的数量来选择使用浮动按钮还是扁平按钮。而且应该避免过多的层叠。

最后,检查你的布局。 一个容器应该只使用一种类型的按钮。 只在比较特殊的情况下(比如需要强调一个浮起的效果)才应该混合使用多种类型的按钮。

更多内容可以看原文:http://www.google.com/design/spec/components/buttons.html

接下来我们来分析下我们的实现效果--ButtonFlat和ButtonRectangle

在这里再次感谢开源作者:https://github.com/navasmdc/MaterialDesignLibrary

先说下ButtonRectangle

public class ButtonRectangle extends Button

继承于Button但是不是Android源生的Button,作者自己写了个Button我们看下去

public abstract class Button extends CustomView

一个抽象类,又继承于 另一个类 CustomView,那我们再看下去

public class CustomView extends RelativeLayout

OK,这下应该到底了,Custom应该是Button的基类然后ButtonRectangle去实现他父类的一系列抽象方法。(读优秀的源码还是很重要的,加深理解和拓宽思路)

	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);
	}

12-23行,构造函数(初始化),声明一个禁用的颜色,一个beforeBackground变量,还有XML配置的标签内容

<span style="white-space:pre">	</span>@Override
	public void setEnabled(boolean enabled) {
		super.setEnabled(enabled);
		if(enabled)
			setBackgroundColor(beforeBackground);
		else
			setBackgroundColor(disabledBackgroundColor);
		invalidate();
	}

25-33 重写了的setEnabled方法,自定义在可点击和不可点击情况下的着色

<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;
	}

35-47 声明一个动画的波尔变量,根据调用开启/关闭动画来对波尔值进行修改

<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);
	}

35-53 初始我们的Button,并且设置了背景的形状以及一个大小参数

	// Set atributtes of XML to View
	abstract protected void setAttributes(AttributeSet attrs);

	// ### RIPPLE EFFECT ###

	float x = -1, y = -1;
	float radius = -1;

55-61 获取参数用的抽象方法,供子类实现,再下面是波纹特效的实现了(期盼已久,暂时还不知道这x y 是什么,继续看下去)

	@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;
	}

63-101 这里就是我们的具体实现了,我们来好好读一下。

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;
		}
	}

103-110,是否为焦点的判断,如果不是一切还原。

<span style="white-space:pre">	</span>@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		// super.onInterceptTouchEvent(ev);
		return true;
	}

112-116,干预Touch的处理,之后的时间不会被传递,可以参考:http://blog.csdn.net/lvxiangan/article/details/9309927

	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;
	}

118-138,具体绘制图像的操作。

首先画了个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);
	}

145-153,涟漪效果的颜色汲取

	@Override
	public void setOnClickListener(OnClickListener l) {
		onClickListener = l;
	}

156-158,接收点击事件的回调

	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
		}
	}

161-174 设置背景色,期间是掺杂了一个自绘bg的颜色设置,这边不进去细说了。

再下面就是一系列的set方法了,我们来分析下刚才那一系列的实现

首先,我们的圈必须是在手指触发事件之后再绘制,并且离开空间范围内的触发是无效的不会触发动画效果,只有手指的触摸圆圈,这个圆圈的大小取决于getHeight/规定值的算法,规定值我们可以设置,这个父类构建了我们按钮所需的动画和计算的基础。

最后我们来说下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()));
	}

41-120行,读取一系列XML传来的内容,包括系统的标签,以及自定义标签,如果没有,就设置预设的参数。

	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();
		}
	}

135-148行

判断是否有接触,如果没有就不绘制(作者拿X作为比较,其实X Y都一样,不为-1就是接触了,没接触(或接触区域不正确)的时候均为-1)

接着画了2个方块和我们之前计算的圆圈做组合效果。

总结:

实现,其实并不是太难,关键是需要更好的思考怎么实现更好,这里大致的再解释下流程。

首先,我们有一个大的方块他是RelativeLayout。

然后我们中间用OnTouchEvent来实现用户的触控过程和触控监听。

不断的绘制用户移动的触控点的圆。

当用户以合理的方式,在符合逻辑的位置up手指的时候出发画布的涟漪效果,这边使用色差和2个方块+圆变大过程的效果来呈现的。


过程中可能有我语言表达的问题,欢迎大家提出。

时间: 2024-11-03 21:25:37

Material Design学习之 Button(详细分析,传说中的水滴动画)的相关文章

Material Design学习之 Snackbars(详细分析,Toast的加强版)

转载请注明出处:王亟亟的大牛之路 昨天把Material Design Button部分的内容分析完了,不知道大家理解了他的实现没有.如果没看的话,可以看下,传送门:http://blog.csdn.net/ddwhan0123/article/details/50555958 这几篇关于Material Design文章的代码几乎都是Git上摘录的,我做的事主要是分享给大家+解释分析. 昨天有小伙伴看完后希望我像以前一样把按钮那一部分的代码单独提取出来单独打成一个包,想单独使用或者学习而不是去

Material Design学习之 CheckBox(详细分析,富有表现力)

转载请注明出处:王亟亟的大牛之路 这些天一直在讲Material Design控件的内容,今天继续,说说CheckBox(妈蛋,好冷),上一篇的传送门:http://blog.csdn.net/ddwhan0123/article/details/50560638 老规矩,两部分,第一部分理论知识,第二部分代码 选择控制器 选择控制器允许用户选择选项.有三种类型:复选框.单选框以及开/关切换.选择控制器使用主题同样的颜色.(待会的代码主要讲的是单选按钮) 复选框 单选按钮 切换开关 官方对呈现的

Material Design学习之 Sliders(详细分析,悬空气球显示进度值,附带Eclipse可以jar)

转载请注明出处:王亟亟的大牛之路 Material Design系列的文章这是第五篇,今天讲滑块控件(Sliders). 之前的传送门:http://blog.csdn.net/ddwhan0123/article/details/50578348(代码实现都靠画,学好View还是很重要的) 老规矩,先说下理论部分 滑块控件(Sliders,简称滑块)可以让我们通过在连续或间断的区间内滑动锚点来选择一个合适的数值.区间最小值放在左边,对应的,最大值放在右边.滑块(Sliders)可以在滑动条的左

Material Design学习之 Switch(详细解释)

转载请注明出处:王亟亟的大牛之路 继续这一系列的Material Design之行,昨天讲的是Sliders链接如下:http://blog.csdn.net/ddwhan0123/article/details/50586510 今天讲的是Switch,本来有考虑把它和CheckBox一起做了,但是毕竟实现不同,还是分开做吧,废话不多,开始正题 开关 On/off 开关切换单一设置选择的状态.开关控制的选项以及它的状态,应该明确的展示出来并且与内部的标签相一致.开关应该单选按钮呈现相同的视觉特

Material Design学习之 Bottom Sheets (顺便提提CoordinatorLayout)

转载请注明出处:王亟亟的大牛之路 昨天连续上了2篇介绍第三方库的文章,正直好久没提交自己写东西了,那么就补一篇之前MD系列漏的部分 Bottom Sheets Bottom Sheets–底部动作条 底部动作条(Bottom Sheets)是一个从屏幕底部边缘向上滑出的一个面板,使用这种方式向用户呈现一组功能.底部动作条呈现了简单.清晰.无需额外解释的一组操作. 在一个标准的列表样式的底部动作条(Bottom Sheets)中,每一个操作应该有一句描述和一个左对齐的 icon.如果需要的话,也可

Material Design学习之 ProgreesBar

转载奇怪注明出处:王亟亟的大牛之路 继续我们Material Design的内容,这一篇讲的是进度条,上一篇是Switch地址如下:http://blog.csdn.net/ddwhan0123/article/details/50592579 进度和动态 在用户可以查看并与内容进行交互之前,尽可能地减少视觉上的变化,尽量使应用加载过程令人愉快.每次操作只能由一个活动指示器呈现,例如,对于刷新操作,你不能即用刷新条,又用动态圆圈来指示. 指示器类型 在操作中,对于完成部分可以确定的情况下,使用确

Material Design学习之 Camera

转载请注明出处:王亟亟的大牛之路 年后第一篇,自从来了某司产量骤减,这里批评下自己,这一篇的素材来源于老牌Material Design控件写手afollestad的 https://github.com/afollestad/material-camera 开篇前,继续安利,你懂的:https://github.com/ddwhan0123/Useful-Open-Source-Android (最近把 6.0授权部分单独罗列出来了) 介绍代码之前先贴下效果图 如何使用 先是添加依赖让mave

Material Design学习之 EditText (功能强大,优于系统自带,感谢“扔物线”)

转载请注明出处:王亟亟的大牛之路 继续之前的Material Design历程,今天是EditText,素材来源于http://www.rengwuxian.com/post/materialedittext(那么代码解释部分大家可以看原作者的文档,我在这里把理论知识灌输下就OK了,作者做的很全面,我都不知道要讲什么了 只能6666666) 大牛的这个库已经有了广泛的认知度和认可,EditText部分就拿他的作为比较推崇的演示版本. 因为大牛已经做了Jar包的支持,所以平时的拆的工作都省了,要直

Material Design学习之 Dialog(顺便把前两天AppBarLayout没讲的部分提一提)

转载请注明出处:王亟亟的大牛之路 继续之前的MD系列的内容,今天说Dialog,不知道还能翻几篇,反正这一系列都说完了话就找点别的内容整整. Dialogs (提示框)用于提示用户作一些决定,或者是完成某个任务时需要的一些其它额外的信息. Dialog可以是用一种 取消/确定 的简单应答模式,也可以是自定义布局的复杂模式,比如说一些文本设置或者是文本输入 . 官方的呈现,像这样 Dialog 包含了一个标题(可选),内容 ,事件. 标题:主要是用于简单描述下选择类型.它是可选的,要需要的时候赋值