編輯:關於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(); } }
為了使用方便,我把氣泡的實現和控件的實現分開了,這樣之後不管實現哪種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); } }
獲取點擊事件是在onTouchEvent裡面
最後繪制這裡選擇了onDraw方法
如果看過前面兩篇博客了,這裡還是紀錄了兩個地方:
1??關於繪制其實有好幾個方法可以選用,看下view繪制流程:先繪制背景,再繪制自己(onDraw),接著繪制子元素(dispatchDraw),最後繪制一些裝飾等比如滾動條(onDrawScrollBars)
因為這裡是要把繪制出來的氣泡做背景,所以要在氣泡繪制完成才去繪制view自身的一些東西,所以在onDraw裡面最合適了
2??關於坐標,由於受前面第一篇博客裡面布局重寫時對坐標處理的影響,在這裡浪費了些時間,其實在view的重寫裡面,重繪的時候使用坐標,只需要知道view的寬高就夠了,因為onDraw傳進來的canvas就是控件本身的大小,
所以不需要像布局裡面那樣對畫布進行裁剪,只要直接在上面畫就行了。坐標系直接自己按照寬高去建立就行了,左上角是原點。
更多動態視圖MoreNewsView經常看朋友圈的動態,有的動態內容較多就只展示前面一段,如果用戶想看完整的再點擊展開,這樣整個頁面的動態列表比較均衡,不會出現個別動態占
上周有個朋友給建議說講講換膚吧,真巧這周公司的工作安排也有這個需求,換的地方之多之繁,讓人傷神死了。正所謂磨刀不誤砍柴工,先磨下刀,抽出一個工具類,寫了個關於換膚的簡單d
以下資源來sharesdk官方demo中的Sample 1,同登錄一樣配置ShareSDK.xml和AndroidManifest.xml【各平台申請好的key】【上
1.下拉列表Spinner 1.1.activity_main.xml Spinner是下拉列表的