Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> android之View和LinearLayout的重寫(實現背景氣泡和波紋效果)

android之View和LinearLayout的重寫(實現背景氣泡和波紋效果)

編輯:關於Android編程

前兩天看了仿android L裡面水波紋效果的兩篇博客

 

Android L中水波紋點擊效果的實現

Android自定義組件系列【14】——Android5.0按鈕波紋效果實現


第一篇是實現了一個水波紋布局,放在裡面的所有控件點擊後都會出現波紋效果

第二篇是實現了一個水波紋view,點擊之後自身會出現波紋效果

 

根據對這兩篇博客的理解,我自己實現了一個類似的東西,沒找到合適的錄屏軟件,只好把波紋的速度調快了很多才錄下來,能看出來啥意思,不調速度的話還算比較優雅。

/

就是像上面這樣一個控件,裡面的背景用的是一個重寫的TextView,背景就一直有一個不斷“呼吸”的氣泡。

這裡就聯系前面兩篇博客(建議先去看下),介紹下在View和ViewGroup中實現背景動畫,同時紀錄下自己對裡面知識點的理解。

就暫時把這種效果命名成會呼吸的氣泡。(後來發現這個圖不動,好吧不知為啥不浪費時間了,就自己想象下這個整個背景有一個圓,不停的放大縮小放大縮小,圓心隨機,這個剛好是隨機到左上角位置了)

 

首先是ViewGroup

這裡就直接以第一篇博客為例,紀錄下自己的理解

1.獲取坐標,這個是比較重要的一步,之後判斷點擊的控件還有繪制波紋都需要用到

 

@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		super.onLayout(changed, l, t, r, b);
		this.getLocationOnScreen(mLocationInScreen);
	}

這裡獲取到的是當前布局左上角在整個屏幕中的坐標

 

2.獲取點擊到的控件,這裡的獲取就需要根據坐標挨個判斷了,在dispatchTouchEvent方法裡面會傳入當前點擊事件MotionEvent,他會帶入當前點擊的坐標,這裡有兩種獲取方式,一種是getX,一種是getRawX,在view的坐標系裡面說到這兩種的區別了,為了計算點擊事件,這裡要獲取的是相對屏幕的坐標,其實在布局的重寫裡面整片都是使用相對屏幕的坐標,因為布局會出現嵌套,嵌套之後相對坐標就不對了,所以全都使用相對屏幕的坐標去計算。獲取到點擊事件的坐標,就可以拿坐標去找到所點擊的控件了。整個過程博客裡面已經很詳細了。

3.拿到點擊的控件之後就要在控件上面繪制波紋了,這裡代碼還是貼一下

 

// view繪制流程:先繪制背景,再繪制自己(onDraw),接著繪制子元素(dispatchDraw),最後繪制一些裝飾等比如滾動條(onDrawScrollBars)
	// 為了防止繪制繪制的子元素把波紋擋住,這裡選擇在子元素繪制完成再繪制波紋
	@Override
	protected void dispatchDraw(Canvas canvas) {
		super.dispatchDraw(canvas);
		if (!mShouldDoAnimation || mTargetWidth < 0 || mTouchTarget == null) {
			return;
		}
		if (mRevealRadius > mMinBetweenWidthAndHeight / 2) {// 當半徑超過短邊之後,增加擴散速度盡快完成擴散
			mRevealRadius += mRevealRadiusGap * 4;
		} else {// 波紋當半徑遞增擴散
			mRevealRadius += mRevealRadiusGap;
		}
		this.getLocationOnScreen(mLocationInScreen);// 獲取本布局的坐標---1??
		int[] location = new int[2];
		mTouchTarget.getLocationOnScreen(location);// 獲取點擊控件的坐標---2??
		int left = location[0] - mLocationInScreen[0];//---3??
		int top = location[1] - mLocationInScreen[1];
		int right = left + mTouchTarget.getMeasuredWidth();
		int bottom = top + mTouchTarget.getMeasuredHeight();
		canvas.save();
		canvas.clipRect(left, top, right, bottom);//---4??
		canvas.drawCircle(mCenterX, mCenterY, mRevealRadius, mPaint);
		canvas.restore();
		if (mRevealRadius <= mMaxRevealRadius) {
			postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);//---5??
		} else if (!mIsPressed) {
			mShouldDoAnimation = false;
			postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);
		}
	}

整個繪制的過程就是上面這樣,還是重點關注下畫布的切割和坐標的計算,繪制流程和半徑的計算之類的看看就明白了

 

1??獲取的是布局左上角的坐標

2??獲取的是控件左上角的坐標

3??控件的坐標減去布局的坐標,就是布局在控件上的相對坐標,這裡的相對坐標其實就是getLeft和getTop的概念,但是不能這麼用,因為有可能控件與布局之間還有嵌套別的布局

4??在3??中已經獲取到了控件相對於布局的坐標,這裡就在布局的畫布上把控件對應的位置切割下來,然後在上面畫圓,切割是為了提高性能

5??這裡畫完圓之後要馬上畫下一個半徑更大的圓,從而達到擴散的效果,所以要postInvalidateDelayed去刷新,刷新的時候只刷新控件所對應的那個區域,也是為了提高性能

基本上就這些吧,原博已經講得比較詳細了,我這裡只是針對自己的理解紀錄下。

 

然後是View

看一下分幾個步驟

1.獲取當前控件的寬高信息(用來初始化氣泡的半徑等信息)

2.獲取點擊事件(用來作為氣泡的圓心,在沒有點擊事件的時候它是隨機坐標作為圓心的,點擊則移動到點擊的位置)

3.繪制氣泡

下面是重寫的BreathTextView代碼

 

public class JasonBreathTextView extends TextView {

	private JasonBreathCircle breathCircle;

	public JasonBreathTextView(Context context) {
		super(context);
	}

	public JasonBreathTextView(Context context, AttributeSet attrs) {
		super(context, attrs);
		breathCircle = new JasonBreathCircle(context);
	}

	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		// TODO Auto-generated method stub
		super.onSizeChanged(w, h, oldw, oldh);
		breathCircle.initParameters(this);
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		breathCircle.setCircleCenter((int) event.getX(), (int) event.getY());
		return super.onTouchEvent(event);
	}

	@Override
	protected void onDraw(Canvas canvas) {
		breathCircle.draw(canvas);
		super.onDraw(canvas);
	}

	/**
	 * 開始水紋效果
	 */
	public void startReveal() {
		breathCircle.start();
	}

	/**
	 * 停止水紋效果
	 */
	public void stopReveal() {
		breathCircle.stop();
	}

}

這個就是圖中使用的繼承自TextView的控件。

 

為了使用方便,我把氣泡的實現和控件的實現分開了,這樣之後不管實現哪種View都可以直接使用分離出來的氣泡類,使用方法就像上面這樣,只需要把view控件本身傳給氣泡,氣泡就會在控件上繪制了。

氣泡BreathCircle的代碼如下:

 

public class JasonBreathCircle {
	private static int DIFFUSE_GAP = 2; // 擴散半徑增量
	private static final int INVALIDATE_DURATION = 10; // 每次刷新的時間間隔

	private Context mContext;
	private boolean needToDrawReveal = false;// 繪畫標志位
	// 圓形自身的一些屬性
	private boolean isLargerMode = true;// 呼吸模式
	private Paint mPaintReveal;// 畫筆
	private int mCircleCenterX;// 圓心x
	private int mCircleCenterY;// 圓心y
	private int mCurRadius;// 當前半徑
	private int mMaxRadius;// 最大半徑
	// 依附的控件的一些屬性,利用高度寬度計算當前觸摸點的位置
	private View mParentView;// 依附的控件
	private int mParentHeight;// 控件高度
	private int mParentWidth;// 控件寬度

	// ================初始化方法(必須調用)===============

	/**
	 * 實例化一個圓,之後要調用initParameters初始化該圓的屬性,再之後就可以draw了
	 * 
	 * @param context
	 */
	public JasonBreathCircle(Context context) {
		mContext = context;
		initPaint();
	}

	/**
	 * 傳入view,用來初始化坐標,半徑,默認以中心為圓心開始畫圓
	 * 
	 * @param view
	 */
	public void initParameters(View view) {
		this.mParentView = view;
		// 獲取當前依附控件的屬性
		mParentHeight = mParentView.getHeight();
		mParentWidth = mParentView.getWidth();
		// 初始化圓的屬性
		mMaxRadius = (int) Math.hypot(view.getHeight(), view.getWidth()) / 2;
		// 控件的寬度高度求出初始圓心
		mCircleCenterX = mParentWidth / 2;
		mCircleCenterY = mParentHeight / 2;
	}

	/**
	 * 傳入畫布
	 * 
	 * @param canvas
	 */
	public void draw(Canvas canvas) {
		if (needToDrawReveal) {
			canvas.save();
			canvas.drawCircle(mCircleCenterX, mCircleCenterY, mCurRadius,
					mPaintReveal);
			canvas.restore();
			if (isLargerMode && mCurRadius < mMaxRadius) {
				mCurRadius += DIFFUSE_GAP;// 波紋遞增
				postRevealInvalidate();
			} else if (mCurRadius > 0 && !isLargerMode) {
				// 畫完一個周期從頭再畫
				mCurRadius -= DIFFUSE_GAP;// 波紋遞增
				postRevealInvalidate();
			} else {// 轉換模式
				isLargerMode = !isLargerMode;
				// 隨機選擇坐標作為圓心,從0到最右邊中間取x,從0到底邊取y
				setCircleCenter(JasonRandomUtil.nextInt(0, mParentWidth),
						JasonRandomUtil.nextInt(0, mParentHeight));
				// 圓心更換後,縮小前,把當前半徑設為最大,防止邊上出現空白覆蓋不滿
				if (!isLargerMode) {
					mCurRadius = mMaxRadius;
				}
				postRevealInvalidate();
			}
		}
	}

	// ===============對外接口===============
	/**
	 * 開始呼吸
	 */
	public void start() {
		if (needToDrawReveal) {
			return;
		}
		needToDrawReveal = true;
		postRevealInvalidate();
	}

	/**
	 * 停止呼吸
	 */
	public void stop() {
		if (!needToDrawReveal) {
			return;
		}
		needToDrawReveal = false;
		reset();
		postRevealInvalidate();
	}

	/**
	 * 設置圓心
	 * 
	 * @param x
	 * @param y
	 */
	public void setCircleCenter(int x, int y) {
		mCircleCenterX = x;
		mCircleCenterY = y;
		mMaxRadius = JasonRadiusUtil.getMaxRadius(mCircleCenterX,
				mCircleCenterY, mParentWidth, mParentHeight);
	}

	/**
	 * 設置畫圓為空心還是實心,默認實心
	 * 
	 * @param isHollow
	 */
	public void setHollow(boolean isHollow) {
		mPaintReveal.setStyle(isHollow ? Paint.Style.STROKE : Paint.Style.FILL);
	}

	// ================內部實現===============
	/**
	 * 重置
	 */
	private void reset() {
		mCurRadius = 0;
		isLargerMode = true;
	}

	/**
	 * 初始化畫筆
	 */
	private void initPaint() {
		mPaintReveal = new Paint();
		mPaintReveal.setColor(mContext.getResources().getColor(
				R.color.jason_bg_common_green_light));
		mPaintReveal.setAntiAlias(true);
	}

	/**
	 * 重繪
	 */
	private void postRevealInvalidate() {
		mParentView.postInvalidateDelayed(INVALIDATE_DURATION);
	}
}

獲取控件的寬高信息是在onSizeChanged這個方法中,這裡會有寬高信息可以去獲取

 

獲取點擊事件是在onTouchEvent裡面

最後繪制這裡選擇了onDraw方法

如果看過前面兩篇博客了,這裡還是紀錄了兩個地方:

1??關於繪制其實有好幾個方法可以選用,看下view繪制流程:先繪制背景,再繪制自己(onDraw),接著繪制子元素(dispatchDraw),最後繪制一些裝飾等比如滾動條(onDrawScrollBars)

因為這裡是要把繪制出來的氣泡做背景,所以要在氣泡繪制完成才去繪制view自身的一些東西,所以在onDraw裡面最合適了

2??關於坐標,由於受前面第一篇博客裡面布局重寫時對坐標處理的影響,在這裡浪費了些時間,其實在view的重寫裡面,重繪的時候使用坐標,只需要知道view的寬高就夠了,因為onDraw傳進來的canvas就是控件本身的大小,

所以不需要像布局裡面那樣對畫布進行裁剪,只要直接在上面畫就行了。坐標系直接自己按照寬高去建立就行了,左上角是原點。

 

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved