我要介紹的是一個 能旋轉的view,說這個view能旋轉有點不切實際,那是視覺效果,其實是對圖片的旋轉。目前它只支持圖片。你可以把它認為是一個能響應手勢旋轉的View。
它的功能有:
1.會響應手勢旋轉
2.該view模擬真實羅盤旋轉:a.旋轉的時候會有慣性,繼續旋轉,而且是減速旋轉b.旋轉期間手指扳動羅盤,能加速羅盤旋轉c.當羅盤在旋轉的時候,手指按住羅盤,它會有剎車的效果。
效果截圖:
為了形象點我用了一張風車的圖作為例子
技術要點
1.需要擴展一個view,重寫ondraw(),onTouchEvent(),onMeasure(),onDetachedFromWindow()方法
a.onDraw():主要是控制圖片旋轉繪圖
b.onTouchEvent():主要是監聽手勢
c.onMeasere():用來測量view的長寬,在xml里最好配置成wrap_content,因為如果為固定值可能會因為長寬不夠,導致顯示不全
d.onDetachedFromWindow():用來回收bitmap
2.需要通過handler來處理慣性
3.需要一個速度分析器,來分析手勢離開時的瞬時速度
4.需要用到圓和三角函數的知識:如反正切函數,弧度等
技術難點分析
1.如何擴展這個View
a.View的旋轉圖片的設置
我們可以提供一個方法來設置旋轉的圖片,並定義旋轉圖片的成員變量,這里我將它命名為rotaBitmap
public void setRotatBitmap(Bitmap bitmap) { rotatBitmap = bitmap; initSize(); postInvalidate(); } public void setRotatDrawableResource(int id) { BitmapDrawable drawable = (BitmapDrawable)getContext().getResources().getDrawable(id); setRotatDrawable(drawable); } public void setRotatDrawable(BitmapDrawable drawable) { rotatBitmap = drawable.getBitmap(); initSize(); postInvalidate(); }
b.View長寬確認
有了圖片就可以確認這個view的大小了,這里view的大小不是image的大小,因為還要考慮旋轉,一個矩形要確保旋轉360度都能被看見,那這個區域應該是個正方形,而且這個正方形的內切圓半徑是這個矩形的對角線一半。不知道能否說明白,還是我畫圖吧。
通過上圖,應該很容易發現,這個view的長度應該是被旋轉圖的對角線的長度
這樣我們可以加上這樣一段代碼:
private void initSize() { if (rotatBitmap == null) { // throw new NoBitMapError("Error,No bitmap in RotatView!"); return; } width = rotatBitmap.getWidth(); height = rotatBitmap.getHeight(); maxwidth = Math.sqrt(width * width + height * height); o_x = o_y = (float)(maxwidth / 2);//確定圓心坐標 }
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // TODO Auto-generated method stub super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 它的寬高不是圖片的寬高,而是以寬高為直角的矩形的對角線的長度 setMeasuredDimension((int)maxwidth, (int)maxwidth); }
c.旋轉原理
圖片的旋轉是在ondraw()里實現的,通過一個變量:deta_degree 來控制旋轉的度數
/** * 當前圓盤所轉的弧度(以該 view 的中心為圓點) */ float deta_degree;
然后用Matrix來控制旋轉圖片,主要是preRotate(deta_degree)這里的單位是度,360度為一圈,最后把旋轉的圖畫到畫布上
@Override protected void onDraw(Canvas canvas) { Matrix matrix = new Matrix(); // 設置轉軸位置 matrix.setTranslate((float)width / 2, (float)height / 2); // 開始轉 matrix.preRotate(deta_degree); // 轉軸還原 matrix.preTranslate(-(float)width / 2, -(float)height / 2); // 將位置送到view的中心 matrix.postTranslate((float)(maxwidth - width) / 2, (float)(maxwidth - height) / 2); canvas.drawBitmap(rotatBitmap, matrix,paint); super.onDraw(canvas); }
考慮到它的周期為360,如果detaDegree的度數太大可能會越界,我們可以做一個於求余處理,讓它的值在-360到360之間
/** * 通過此方法來控制旋轉度數,如果超過360,讓它求余,防止,該值過大造成越界 * * @param added */ private void addDegree(float added) { deta_degree += added; if (deta_degree > 360 || deta_degree < -360) { deta_degree = deta_degree % 360; } }
這里的動畫是通過不停的走ondraw()方法刷新,產生的效果,類似放電影一樣
d.view的手勢響應
當用戶觸摸它時會響應onTouch事件,在onTouch里分析手指的坐標,通過坐標算出與圓心的夾角
下圖為手指與view中心的夾角(這里的原點就是view的旋轉中心):
每次手指滑動或是松開都會計算它與原點的夾角,這個方法可以通過反正切函數求出來,詳情:
/** * 計算以(src_x,src_y)為坐標圓點,建立直角體系,求出(target_x,target_y)坐標與x軸的夾角 * 主要是利用反正切函數的知識求出夾角 * * @param src_x * @param src_y * @param target_x * @param target_y * @return */ float detaDegree(float src_x, float src_y, float target_x, float target_y) { float detaX = target_x - src_x; float detaY = target_y - src_y; double d; //坐標在四個象限里 if (detaX != 0) { float tan = Math.abs(detaY / detaX); if (detaX > 0) { //第一象限 if (detaY >= 0) { d = Math.atan(tan); } else { //第四象限 d = 2 * Math.PI - Math.atan(tan); } } else { if (detaY >= 0) { //第二象限 d = Math.PI - Math.atan(tan); } else { //第三象限 d = Math.PI + Math.atan(tan); } } } else { //坐標在y軸上 if (detaY > 0) { //坐標在y>0上 d = Math.PI / 2; } else { //坐標在y<0上 d = -Math.PI / 2; } } return (float)((d * 180) / Math.PI); }
通過以上方法,可以把每次移動的時候的夾角求出來,把當前的夾角和上次的手指夾角坐差運算就能求出手指相對圓心旋轉的角度增量,得到這個角度增量就可以通過調用
這前提過的addDegree()方法,改變圖片的角度,然后調用invalidate()方法重繪,就實現了羅盤隨手指旋轉的效果。
在這里賦上ontouch處理down和move事件的代碼(up事件是用來處理慣性用的):
@Override public boolean onTouchEvent(MotionEvent event) { // TODO Auto-generated method stub if (rotatBitmap == null) { throw new NoBitMapError("Error,No bitmap in RotatView!"); } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { down_x = event.getX(); down_y = event.getY(); current_degree = detaDegree(o_x, o_y, down_x, down_y); break; } case MotionEvent.ACTION_MOVE: { down_x = target_x = event.getX(); down_y = target_y = event.getY(); float degree = detaDegree(o_x, o_y, target_x, target_y); // 滑過的弧度增量 float dete = degree - current_degree; // 如果小於-90度說明 它跨周了,需要特殊處理350->17, if (dete < -270) { dete = dete + 360; // 如果大於90度說明 它跨周了,需要特殊處理-350->-17, } else if (dete > 270) { dete = dete - 360; } addDegree(dete); current_degree = degree; invalidate(); break; }
e.View的瞬時速度獲取
為了得到瞬時速度,我的思路是通過一個固定長度的2維數組,把手指與原點的最近幾次夾角增量和時間點記錄下來,通過這幾個夾角增量和時間點,可以算出平均速度,因為手指滑動的時候,響應ontouch事件次數非常多,我們可以把最后幾次ontouch記錄下的數據,取平均值,把它認為是瞬時速度。
這里用到了一點物理知識:
假如上圖為record記錄的4組數據,t代表時間點,表示產生這個事件的時間,d代表手指與圓心夾角的增量,它是這次夾角與上次夾角的差值
這樣我們可以把t=t3-t0算出經過的時間,把sum=d1+d2+d3算出這段時間一共經歷過的弧度
但需要注意一個細節:d0是無效的
這里給出計算速度 的代碼:
因為考慮不能讓速度太快,所以給出了一個最大值
/** * 最大速度 */ public static final double max_speed = 8; /** * 通過數組里所裝載的數據分析出即時速度<br> * 原理是:計算數組里的時間長度和增量的總數,然后求出每毫秒所走過的弧度<br> * 當然不能超過{@link VRecord#max_speed} * * @return */ public double getSpeed() { if (addCount == 0) { return 0; } int maxIndex = Math.min(addCount, length) - 1; if ((record[0][1] - record[maxIndex][1]) == 0) { return 0; } double detaTime = record[0][1] - record[maxIndex][1]; double sumdegree = 0; for (int i = 0; i < length - 1; i++) { sumdegree += record<I>[0]; // System.out.println(record[0]); } // System.out.println("----------"); // System.out.println(sumdegree); // System.out.println(detaTime); double result = sumdegree / detaTime; if (result > 0) { return Math.min(result, max_speed); } else { return Math.max(result, -max_speed); } // System.out.println("v=" + result); }
講到這我要提一下,這個二維數組是如何做到獲取最近的數據,如果超過容量,它將把原來的數據丟棄,我想直接上代碼,大家就能看懂吧
/** * 二維數組,1.保存弧度增量.2.保存產生這個增量的時間點 */ double[][] record = new double[length][2]; /** * 為二維數組裝載數據<br> * 注:通過此方法,有個特點,能把最后的length組數據記錄下來,length以外的會丟失 * * @param detadegree * @param time */ public void add(double detadegree, double time) { for (int i = length - 1; i > 0; i--) { record[0] = record[i - 1][0]; record[1] = record[i - 1][1]; } record[0][0] = detadegree; record[0][1] = time; addCount++; }
f.View慣性處理
這里的慣性處理就是用到加速度了,再用handler發消息,控制它不停減速,減到它為零為止就停止發消息
下面是handler代碼
@Override public void handleMessage(Message msg) { double detaTime = System.currentTimeMillis() - currentTime; switch (msg.what) { case play: { //如果是順時針 if (isClockWise) { speed = speed - a * detaTime;//減速 if (speed <= 0) { return; } else { handler.sendEmptyMessageDelayed(play, delayedTime); } } else { speed = speed + a * detaTime; if (speed >= 0) { return; } else { handler.sendEmptyMessageDelayed(play, delayedTime); } } addDegree((float)(speed * detaTime + (a * detaTime * detaTime) / 2));//高中物理算路程S=vt+at2 // if (a < a_max) { // a = (float)(a + a_add*detaTime); // System.out.println("a:"+a); // } currentTime = System.currentTimeMillis(); invalidate(); break; } case stop: { speed = 0; handler.removeMessages(play); } } super.handleMessage(msg); } };
轉載請寫以下地址:http://www.eoeandroid.com/thread-207498-1-1.html
源碼下載:MyRotation.rar