之前一直看QQ的未讀消息拖拽消失設計得很好,我一直覺得那個設計很好,他們的UI是真心強,於是,我也一直想寫個一樣的玩意來玩玩。最近剛好在複習View相關的知識,就拿這個來練手,下面先來看實現的效果圖:
這是我希望實現的效果,這個效果的實現在第二個圖能看出一點端倪。這裏面的曲線繪製,使用的是貝塞爾曲線。下面用幾個例子簡單介紹下貝塞爾曲線,參考網上大神的文章,我對原文大神的代碼做了一點點修改。
什麼是貝塞爾曲線?
貝賽爾曲線(Bézier曲線)是電腦圖形學中相當重要的參數曲線。更高維度的廣泛化貝塞爾曲線就稱作貝塞爾曲面,其中貝塞爾三角是一種特殊的實例。貝塞爾曲線於1962年,由法國工程師皮埃爾·貝塞爾(Pierre Bézier)所廣泛發表,他運用貝塞爾曲線來爲汽車的主體進行設計。貝塞爾曲線最初由Paul de Casteljau於1959年運用de Casteljau算法開發,以穩定數值的方法求出貝塞爾曲線。
接下來從一個簡單的二階貝塞爾曲線開始
接下來從一個簡單的二階貝塞爾曲線開始
package com.example.draftcircleviewtest;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Point;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.FrameLayout;
import android.widget.ImageView;
/**
* 貝塞爾曲線的測試
* 藍色的點是輔助點
* @author Administrator
*
*/
public class MyBezierView extends FrameLayout {
private Paint mPaint;
private Path mPath;
private Point startPoint;
private Point endPoint;
private Point assistPoint;
private Context mContext;
public MyBezierView(Context context ) {
this(context,null);
}
public MyBezierView( Context context ,AttributeSet attrs )
{
this(context, attrs , 0);
}
public MyBezierView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
ImageView tipImageView;
private void init( Context context )
{
this.mContext = context;
this.mPaint = new Paint();
this.mPath = new Path();
startPoint = new Point(100, 300);
endPoint = new Point(1100, 300);
assistPoint = new Point(600, 800);
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setStrokeWidth(5);
//設置背景顏色,再設置背景爲空,這樣子是爲了能一開始就draw
//否則會發現onDraw不被調用
setBackgroundColor(Color.WHITE);
getBackground().setAlpha(0);
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
mPaint.setColor(Color.BLACK);
mPaint.setStyle( Style.STROKE );
mPaint.setStrokeWidth( 5);
mPath.reset();
Log.e("MyView", "onDraw");
mPath.moveTo(startPoint.x, startPoint.y);
// 重要的就是這句
mPath.quadTo(assistPoint.x, assistPoint.y, endPoint.x, endPoint.y);
// 畫路徑
canvas.drawPath(mPath, mPaint);
// 畫輔助點
mPaint.setColor(Color.BLUE);
mPaint.setStrokeWidth( 20);
canvas.drawPoint(assistPoint.x, assistPoint.y, mPaint);
canvas.drawPoint(startPoint.x, startPoint.y, mPaint);
canvas.drawPoint(endPoint.x, endPoint.y, mPaint);
super.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
assistPoint.x = (int) event.getX();
assistPoint.y = (int) event.getY();
invalidate();
break;
default:
break;
}
return true;
}
}
效果:
這樣一個二階貝塞爾曲線需要三個點,起點,終點,一個輔助點,初始化畫筆以後,我們就可以在onDraw上繪製曲線,很簡單,就是如下代碼:
mPath.moveTo(startPoint.x, startPoint.y);
// 重要的就是這句
mPath.quadTo(assistPoint.x, assistPoint.y, endPoint.x, endPoint.y);
// 畫路徑
canvas.drawPath(mPath, mPaint);
首先我們把畫筆移動到起點,然後,在路線上設置爲貝塞爾曲線,填入輔助點和終點,最後在canvas上畫出就可以了。拖動的效果實現也很簡單,我們重寫onTouchEvent,捕捉keydown事件和keyup事件,在事件發生的時候,修改assistPoint位置爲事件發生所在的位置就好了。
接下來我們繪製一條複雜一點的貝塞爾曲線。
思路
先根據相鄰點(P1,P2, P3)計算出相鄰點的中點(P4, P5),然後再計算相鄰中點的中點(P6)。然後將(P4,P6, P5)組成的線段平移到經過P2的直線(P8,P2,P7)上。接着根據(P4,P6,P5,P2)的座標計算出(P7,P8)的座標。最後根據P7,P8等控制點畫出三階貝塞爾曲線。
點和線的解釋
黑色點:要經過的點,例如溫度 藍色點:兩個黑色點構成線段的中點 黃色點:兩個藍色點構成線段的中點 灰色點:貝塞爾曲線的控制點 紅色線:黑色點的折線圖 黑色線:黑色點的貝塞爾曲線,也是我們最終想要的效果。
那麼我們知道,我們繪製貝塞爾曲線,除了需要知道p1 p2 p3 還需要知道 P8 P7 P9 (P5下面的那個紅點當成是P9吧),現在的情況是,我們已經知道了P1,P2,,P3,我們需要知道P7 P8 ,其實P7 P8不是固定值,只要確保P7 P8 P9 不在線上就,那麼我們可以這樣來計算P7 P8 P9,我們先算中點,得到P4 P5的值,然後,我們取P6 的值,然後,我們知道P4 P5 P6 在同一條直線上,我們平移P4 P5 P6,讓P6到P2位置,得到P7 P8 ,這樣,只要P1、P2、P3 不在同一直線上,那麼P8 也不會和P1 P2 在同一直線上,P7也不會和P2 P3在同一直線上。按照這個思路,我們可以知道:
P8 = P2 - P6 + P4。
實現的代碼如下:
package com.example.draftcircleviewtest;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.Point;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import android.widget.FrameLayout;
public class MultiBezierView extends FrameLayout {
private static final int LINEWIDTH = 5;
private static final int POINTWIDTH = 10;
private Paint mPaint;
private Path mPath;
private Context mContext;
/** 即將要穿越的點集合 */
private ArrayList<Point> mPoints;
/** 中點集合 */
private ArrayList<Point> mMidPoints;
/** 中點的中點集合 */
private ArrayList<Point> mMidMidPoints;
/** 移動後的點集合(控制點) */
private ArrayList<Point> mAssistPoints;
public MultiBezierView(Context context) {
this(context, null);
}
public MultiBezierView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MultiBezierView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mContext = context;
init();
}
private void init()
{
mPaint = new Paint();
mPath = new Path();
mPaint.setStyle( Style.STROKE );
mPaint.setStrokeWidth( 5 );
mPaint.setDither(true);
mPaint.setAntiAlias( true );
getScreenParams();
setBackgroundColor(Color.WHITE);
getBackground().setAlpha(0);
initPoints();
initMidPoints();
initMidMidPoints();
initAssistPoints( mPoints , mMidPoints , mMidMidPoints );
}
private void initPoints()
{
mPoints = new ArrayList<Point>();
int pointWidthSpace = mScreenWidth / 5;
int pointHeightSpace = 100;
for( int i = 0 ; i < 5 ; i ++ )
{
Point point;
// 一高一低五個點
if (i%2 != 0) {
point = new Point((int) (pointWidthSpace*(i + 0.5)), mScreenHeigh/2 - pointHeightSpace);
} else {
point = new Point((int) (pointWidthSpace*(i + 0.5)), mScreenHeigh/2);
}
mPoints.add(point);
}
}
private void initMidPoints()
{
mMidPoints = new ArrayList<Point>();
for( int i = 0 ; i < mPoints.size() -1 ; i ++ )
{
Point midPoint = new Point(( mPoints.get(i).x + mPoints.get( i + 1 ).x )/2,
(mPoints.get(i).y + mPoints.get(i + 1).y)/2);
mMidPoints.add(midPoint);
}
}
private void initMidMidPoints()
{
mMidMidPoints = new ArrayList<Point>();
for( int i = 0 ; i < mMidPoints.size()-1; i ++ )
{
Point midMidPoint = new Point( (mMidPoints.get(i).x + mMidPoints.get(i+1).x )/2,
(mMidPoints.get(i).y + mMidPoints.get(i+1).y )/2);
mMidMidPoints.add(midMidPoint);
}
}
private void initAssistPoints(List<Point> points , List<Point> midPoints , List<Point> midMidPoints )
{
mAssistPoints = new ArrayList<Point>();
for( int i = 0 ; i < points.size() ; i ++ )
{
if( i ==0 || i == points.size()-1 )
{
continue;
}else
{
Point assistBefore = new Point();
assistBefore.x = midPoints.get(i-1).x + (points.get(i).x- mMidMidPoints.get(i-1).x);
assistBefore.y = midPoints.get(i-1).y + (points.get(i).y- mMidMidPoints.get(i-1).y);
Point assistAfter = new Point();
assistAfter.x = midPoints.get(i).x + (points.get(i).x- mMidMidPoints.get(i-1).x);
assistAfter.y = midPoints.get(i).y + (points.get(i).y- mMidMidPoints.get(i-1).y);
mAssistPoints.add(assistBefore);
mAssistPoints.add(assistAfter);
}
}
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
//畫原始點
drawPoints(canvas);
//畫原始點連接的直線
drawLineForPoints(canvas);
//畫中點
drawMidPoints(canvas);
//畫中點的中點即圖中的P6
drawMidMidPoints(canvas);
//畫輔助點
drawAssistPoints( canvas);
drawBezierPath( canvas );
}
private void drawPoints(Canvas canvas)
{
mPaint.setStrokeWidth(POINTWIDTH);
for( Point point : mPoints)
{
canvas.drawPoint(point.x, point.y, mPaint);
}
}
private void drawLineForPoints( Canvas canvas )
{
mPaint.setStrokeWidth(LINEWIDTH);
mPaint.setColor(Color.RED );
mPath.moveTo(mPoints.get(0).x, mPoints.get(0).y);
for( int i = 1 ; i < mPoints.size(); i ++ )
{
mPath.lineTo(mPoints.get(i).x, mPoints.get(i).y);
}
canvas.drawPath(mPath, mPaint);
}
private void drawMidPoints(Canvas canvas)
{
mPaint.setStrokeWidth(POINTWIDTH);
mPaint.setColor(Color.BLUE);
for( Point point : mMidPoints)
{
canvas.drawPoint(point.x, point.y, mPaint);
}
}
private void drawMidMidPoints(Canvas canvas)
{
mPaint.setStrokeWidth(POINTWIDTH);
mPaint.setColor(Color.YELLOW);
for( Point point : mMidMidPoints)
{
canvas.drawPoint(point.x, point.y, mPaint);
}
}
private void drawAssistPoints(Canvas canvas)
{
mPaint.setStrokeWidth(POINTWIDTH);
mPaint.setColor(Color.GRAY);
for( Point point : mAssistPoints)
{
canvas.drawPoint(point.x, point.y, mPaint);
}
}
private void drawBezierPath( Canvas canvas )
{
mPaint.setStrokeWidth(LINEWIDTH);
mPaint.setColor(Color.BLACK);
mPath.reset();
mPath.moveTo(mPoints.get(0).x, mPoints.get(0).y);
for( int i = 0 ; i < mPoints.size()-1 ; i ++ )
{
if( i == 0 )
{
// 第一條爲二階貝塞爾
mPath.quadTo(mAssistPoints.get(i).x, mAssistPoints.get(i).y, mPoints.get(i+1).x, mPoints.get(i+1).y);
}
else//一直到最後一條之前,是三階貝塞爾曲線
if( i < mPoints.size() -2 )
{
mPath.cubicTo(mAssistPoints.get(i*2-1).x,mAssistPoints.get(i*2-1).y,
mAssistPoints.get(i*2).x, mAssistPoints.get(i*2).y,
mPoints.get(i+1).x, mPoints.get(i+1).y);
}
else//最後一條也是二階貝塞爾曲線
if( i == mPoints.size() -2 )
{
mPath.quadTo(mAssistPoints.get(mAssistPoints.size()-1).x, mAssistPoints.get(mAssistPoints.size()-1).y, mPoints.get(i+1).x, mPoints.get(i+1).y);
}
}
canvas.drawPath(mPath, mPaint);
}
public int mScreenWidth=-1,mScreenHeigh=-1;
private void getScreenParams( )
{
getWindowHeigh(mContext);
getWindowWidth(mContext);
}
public int getWindowWidth(Context context) {
if( mScreenWidth <= 0 )
{
WindowManager wm = (WindowManager) (context
.getSystemService(Context.WINDOW_SERVICE));
DisplayMetrics dm = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(dm);
mScreenWidth = dm.widthPixels;
}
return mScreenWidth;
}
public int getWindowHeigh(Context context) {
if( mScreenHeigh <=0 )
{
WindowManager wm = (WindowManager) (context
.getSystemService(Context.WINDOW_SERVICE));
DisplayMetrics dm = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(dm);
mScreenHeigh = dm.heightPixels;
}
return mScreenHeigh;
}
}
接下來,我們開始設計粘性的動畫,我們想要的效果圖如下:
其實,這個可以看成是兩個圓+2條二階貝塞爾曲線,經過分析,我們可以得到如下:
那麼,如今的問題就是求垂直於圓心連線的線與圓的交點座標 PA PB PC PD 這四個點座標。
我們很容易證明如下:
點PA的圓心角滿足如下圖:
我們很容易得到如下:
∠α=arctan( (y0-y1)/( x1 - x0 ) ) = -arctan((y1-y0)/(x1-x0))
另γ = -∠α = arctan((y1-y0)/(x1-x0))
令∠β = 270°-∠α ,也就是β = 270°+γ β爲點A的圓心角
B的圓心角爲90-∠α
xA = x0+ r * cos( 270° - ∠α ) =x0+ r*(-sin∠α )= x0-r*sin∠α = x0+r*sin( -∠α ) = x0 + r* sin( arctan((y1-y0)/(x1-x0)) ) = x0 + r * sin γ
yA = x0 + r*sin(270° - ∠α) = x0 -r*cosa = x0 - r*cosγ
同理可以得到B座標
xB = x0-r*sinγ
yB = x0 + r*cosγ
所以我們可以得到下面的代碼**(完整的項目代碼在文章末尾,包括本文所有的代碼)**:
/**大圓的半徑*/
public static final int BIG_CIRCLE_RADIUS = 40;
/**小圓心,固定不變的*/
private Point smallCircleCenter;
/**大圓心*/
private Point bigCircleCenter;
/**小圓上的兩個點,他們的連線垂直兩個圓心的連線。*/
private Point smallCirclePoint1,smallCirclePoint2;
/**大圓上的兩個點,他們的連線垂直兩個圓心的連線。*/
private Point bigCirclePoint1,bigCirclePoint2;
/**當前的圓心距*/
private float centersDistance;
/**當前小圓的半徑*/
private int smallCircleRadius = SMALL_CIRCLE_MAX_RADIUS;
private void calculate()
{
if( bigCircleCenter == null )
return ;
centersDistance = (float) Math.sqrt(Math.pow(smallCircleCenter.x-bigCircleCenter.x,2) + Math.pow(smallCircleCenter.y-bigCircleCenter.y,2)) ;
if( centersDistance >= CENTERS_MAX_DISTANCE )
smallDismiss = true;
int smalldx = (int) (centersDistance/CENTERS_MAX_DISTANCE * ( SMALL_CIRCLE_MAX_RADIUS - SMALL_CIRCLE_MIN_RADIUS ));
smallCircleRadius = SMALL_CIRCLE_MAX_RADIUS- smalldx;
double s1 = 90;
if( bigCircleCenter.x != smallCircleCenter.x )
s1 = Math.atan( (bigCircleCenter.y-smallCircleCenter.y)/( bigCircleCenter.x-smallCircleCenter.x));
float smallOffsetX = (float) (smallCircleRadius * Math.sin(s1));
float smallOffsetY = (float) (smallCircleRadius * Math.cos(s1));
smallCirclePoint1 = new Point();
smallCirclePoint1.x = (int) (smallOffsetX + smallCircleCenter.x);
smallCirclePoint1.y = (int) (smallCircleCenter.y - smallOffsetY);
smallCirclePoint2 = new Point();
smallCirclePoint2.x = (int) ( smallCircleCenter.x-smallOffsetX);
smallCirclePoint2.y = (int) (smallCircleCenter.y + smallOffsetY);
float bigOffsetX = (float) (BIG_CIRCLE_RADIUS * Math.sin(s1));
float bigOffsetY = (float) (BIG_CIRCLE_RADIUS * Math.cos(s1));
bigCirclePoint1 = new Point();
bigCirclePoint1.x = (int) (bigOffsetX + bigCircleCenter.x);
bigCirclePoint1.y = (int) (bigCircleCenter.y - bigOffsetY);
bigCirclePoint2 = new Point();
bigCirclePoint2.x = (int) ( bigCircleCenter.x - bigOffsetX);
bigCirclePoint2.y = (int) (bigCircleCenter.y + bigOffsetY);
midCenter = new Point();
midCenter.x = (bigCircleCenter.x + smallCircleCenter.x )/2;
midCenter.y = (bigCircleCenter.y + smallCircleCenter.y )/2;
}
接下來就是繪製貝塞爾曲線,注意繪製的時候, 要使用lineTo,連接圓上的兩個點,這樣才能形成一個封閉的區間,之後才能填充顏色。
/**
* 繪製貝塞爾曲線
* @param canvas
*/
private void drawBezierPath( Canvas canvas )
{
mPaint.setStrokeWidth(PAINT_LINE_WIDTH);
mPaint.setColor(paintColor);
mPath.reset();
//下面的lineTo不可以少,因爲我們要形成一個封閉的空間,這樣才能填充
mPath.moveTo( smallCirclePoint1.x, smallCirclePoint1.y);
mPath.quadTo(midCenter.x, midCenter.y, bigCirclePoint1.x, bigCirclePoint1.y);
mPath.lineTo(bigCirclePoint2.x, bigCirclePoint2.y);
mPath.quadTo(midCenter.x, midCenter.y, smallCirclePoint2.x, smallCirclePoint2.y);
mPath.lineTo(smallCirclePoint1.x, smallCirclePoint1.y);
canvas.drawPath(mPath, mPaint);
}
當我們設置Paint的Style爲Stroke的時候
上面的代碼效果如下:
當我們設置
mPaint.setStyle( Paint.Style.FILL_AND_STORKE);
填充效果如下:
接下來我們畫圓以後效果:
再加入動畫效果,代碼如下:
//動畫對象
ObjectAnimator animator;
//是否重複播放
boolean isrepeat = false;
public void startAni( )
{
if( lastf == 0.0f)
{
animator = ObjectAnimator.ofFloat(this, "bigCircleCenter", 0, 2.0f);
animator.setRepeatCount(ValueAnimator.INFINITE);
isrepeat = true;
}else
{
animator = ObjectAnimator.ofFloat(this, "bigCircleCenter", lastf, 2.0f);
isrepeat = false;
}
animator.setDuration( (int)(2000*(( 2.0f - lastf)/2.0f)));
animator.start();
}
//上次播放動畫的位置,當lastf<=1.0f執行動畫,當lastf>1.0f的時候,播放逆動畫
float lastf = 0.0f;
public void setBigCircleCenter( float f )
{
int center =0;
int smalldx = 0;
if( f <= 1.0f){
//計算大圓圓心的位置,這個情況下圓心位置在增大
center = (int) (f * 800 + 400);
//計算小圓半徑縮小值,這個時候值在增大
smalldx = (int) ((SMALL_CIRCLE_MAX_RADIUS - SMALL_CIRCLE_MIN_RADIUS)*f);
}else{
//計算大圓圓心的位置,這個值在變小
center = (int)(1200-(f-1.0f)*800);
//計算小圓半徑縮小值,這個時候值在變小
smalldx = (int) ((SMALL_CIRCLE_MAX_RADIUS - SMALL_CIRCLE_MIN_RADIUS)-(SMALL_CIRCLE_MAX_RADIUS - SMALL_CIRCLE_MIN_RADIUS)*(f-1.0f));
}
bigCircleCenter.x = center;
bigCircleCenter.y = center;
smallCircleRadius = SMALL_CIRCLE_MAX_RADIUS- smalldx;
lastf = f;
invalidate();
//暫停恢復動畫以後,先執行一次動畫,動畫結束以後,關閉當前動畫,
//再啓動新的循環動畫
if( !isrepeat)
{
if( lastf ==0.0f || lastf == 2.0f )
{
stopAni();
lastf = 0;
startAni();
}
}
}
當Paint的Style爲Stroke效果如下:
設置Paint的Style爲FILL_AND_STROKE,並且隱藏點,得到效果:
拖拽的實現也很簡單,我們重寫onTouchEvent事件,在Down和Move的時候,修改大圓心的位置,當移動太遠,就隱藏小圓,在ACTION_UP的時候判斷圓心距,如果圓心距不在指定範圍內,就不顯示大圓了,否則,就把大圓迴歸原位,同時如果小圓還顯示着,就執行動畫。下面只放出關鍵的代碼:
onTouchEvent實現代碼如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if( bigCircleCenter == null )
bigCircleCenter = new Point();
bigCircleCenter.x = (int) event.getX();
bigCircleCenter.y = (int) event.getY();
calculate();
if( centersDistance < SMALL_CIRCLE_MAX_RADIUS + 20 )
invalidate();
else{
bigCircleCenter =null;
return false;
}
break;
case MotionEvent.ACTION_MOVE:
if( bigCircleCenter == null )
bigCircleCenter = new Point();
bigCircleCenter.x = (int) event.getX();
bigCircleCenter.y = (int) event.getY();
invalidate();
break;
case MotionEvent.ACTION_UP:
if( bigCircleCenter == null || ( bigCircleCenter.x == beforeX && bigCircleCenter.y == beforeY) )
{
return false;
}
if( smallDismiss && centersDistance < CENTERS_MIN_DISTANCE )
{
smallDismiss = false;
bigCircleCenter = new Point(beforeX, beforeY);
invalidate();
}else if( !smallDismiss)
{
float f = centersDistance / CENTERS_MAX_DISTANCE ;
animator = ObjectAnimator.ofFloat(this, "bigCircleCenter", 0, 1.0f);
animator.setDuration( (int)(1000 * f));
animator.start();
}else
{
stopAni();
bigCircleCenter = null;
invalidate();
}
break;
default:
break;
}
return true;
}
修改前面的動畫執行代碼改爲如下:
/**
* 鬆開的時候,執行回覆的動畫
* @param f
*/
public void setBigCircleCenter( float f )
{
int centerX =0;
int centerY =0;
if( bigCircleCenter.x == 45&&bigCircleCenter.y == 45 )
{
stopAni();
afterAni();
return ;
}
//計算新的中心點
centerX = (int) ( bigCircleCenter.x + (beforeX - bigCircleCenter.x )*f);
centerY = (int) ( bigCircleCenter.y + (beforeY - bigCircleCenter.y )*f);
bigCircleCenter.x = centerX;
bigCircleCenter.y = centerY;
invalidate();
if( f == 1.0f )
{
afterAni();
}
}
下面看下最終的效果:
這個控件還是做的比較簡單,像騰訊那樣做的話,我們還需要重寫onLayout和onSizeChanged方法保存當前控件的佈局參數,然後再使用WindowManager在觸摸的時候添加到窗口上,並且窗口是整個屏幕大小的,這樣就實現氣泡拖拽全局的效果。還可以擴展一些事件接口,這樣更靈活,這些這邊就不實現了,以後有空完善了,再發一份。
代碼還有一些BUG,僅供參考。附上項目的源碼地址:
代碼上的onTouchEvent有問題,請改成博客上的。
http://download.csdn.net/detail/savelove911/9626154