Material Design學習之 Button(具體分析,傳說中的水滴動畫)

轉載請註明出處:王亟亟的大牛之路html


      上一篇大體介紹了Material Design的一些基本概念傳送門:http://blog.csdn.net/ddwhan0123/article/details/50541561java

這一片來詳細學習下里面的內容,這篇分爲兩部分一部分是原理分析,一部分是代碼分析。android

先簡要的介紹一些理論知識,順便溫顧下基礎知識git


button

button由文字和/或圖標組成,文字及圖標必須能讓人輕易地和點擊後展現的內容聯繫起來。
基本的button有三種:
github

  • 懸浮響應button(Floating action button), 點擊後會產生墨水擴散效果的圓形button。
  • 浮動button(Raised button), 常見的方形紙片button,點擊後會產生墨水擴散效果。
  • 扁平button(Flat button)。 點擊後產生墨水擴散效果,和浮動button的差異是沒有浮起的效果。

顏色飽滿的圖標應當是功能性的,儘可能避免把他們做爲純粹裝飾用的元素。算法

在咱們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功能: 它是否是很重要而且應用普遍到需要用上懸浮響應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

繼承於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/規定值的算法。規定值咱們可以設置。這個父類構建了咱們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())); }


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個方塊+圓變大過程的效果來呈現的。


過程當中可能有我語言表達的問題。歡迎你們提出。

相關文章
相關標籤/搜索