Android貝塞爾曲線(xiàn)實(shí)現(xiàn)消息拖拽消失
寫(xiě)在前頭
寫(xiě)消息拖拽效果的文章不少,但是大部分都把自定義View寫(xiě)死了,我們要實(shí)現(xiàn)的是傳入一個(gè)View,每個(gè)View都可以實(shí)現(xiàn)拖拽消失爆炸的效果,當(dāng)然我也是站在巨人的肩膀上來(lái)學(xué)習(xí)的。但個(gè)人覺(jué)得程序員本就應(yīng)該敢于學(xué)習(xí)和借鑒。
源碼地址:源碼Github地址
效果圖

分析(用到的知識(shí)點(diǎn)):
(1)ValueAnimator (數(shù)值生成器) 用于生成數(shù)值,可以設(shè)置差值器來(lái)改變數(shù)字的變化幅度。
(2)ObjectAnimator (動(dòng)畫(huà)生成器) 用于生成各種屬性,布局動(dòng)畫(huà),同樣也可以設(shè)置差值器來(lái)改變效果。
(3)貝塞爾一階曲線(xiàn)
(4)自定義View的基礎(chǔ)知識(shí)
(5)WindowManager 使view拖拽能顯示在整個(gè)屏幕的任何地方,而不是局限于父布局內(nèi)
具體實(shí)現(xiàn)方法
一、首先我們要實(shí)現(xiàn)基礎(chǔ)效果
基礎(chǔ)效果是點(diǎn)擊屏幕任意一點(diǎn)能出現(xiàn)消息拖拽的效果,但是此時(shí)我們不用管我們拖動(dòng)的View,只需要完成大致模型。該部分的難點(diǎn)在于貝塞爾一階曲線(xiàn)的怎么實(shí)現(xiàn)。
基礎(chǔ)效果圖

分析:
(1)點(diǎn)擊任意一點(diǎn)畫(huà)出兩個(gè)圓,和一個(gè)有貝塞爾曲線(xiàn)組成的path路徑
(2)隨著拖動(dòng)距離的增加原點(diǎn)的圓半徑逐漸縮小,當(dāng)距離達(dá)到一定大以后原點(diǎn)的圓和貝塞爾曲線(xiàn)組成的path不再顯示
貝塞爾曲線(xiàn)的畫(huà)法

首先我們需要求出角a的大小,根據(jù)角a來(lái)求到A,B,C,D的坐標(biāo)位子,然后求到控制點(diǎn)E點(diǎn)的坐標(biāo),通過(guò)Path.quadTo()方法來(lái)連接A,B和C,D兩條貝塞爾曲線(xiàn)。
各點(diǎn)坐標(biāo)
A(c1.x+sina*c1半徑,c1.y-cina*c1半徑)
B(c2.x+sina*c2半徑,c2.y-cina*c2半徑)
C(c2.x-sina*c1半徑,c2.y+cina*c1半徑)
D(c1.x-sina*c2半徑,c1.y+cina*c2半徑)
E ((c1.x+c2.x)/2,(c1.y+c2.y)/2)
貝塞爾曲線(xiàn)的path代碼
private Path getBezeierPath() {
double distance = getDistance(mBigCirclePoint,mLittleCirclePoint);
mLittleCircleRadius = (int) (mLittleCircleRadiusMax - distance / 10);
if (mLittleCircleRadius < mLittleCircleRadiusMin) {
// 超過(guò)一定距離 貝塞爾和固定圓都不要畫(huà)了
return null;
}
Path bezeierPath = new Path();
// 求角 a
// 求斜率
float dy = (mBigCirclePoint.y-mLittleCirclePoint.y);
float dx = (mBigCirclePoint.x-mLittleCirclePoint.x);
float tanA = dy/dx;
// 求角a
double arcTanA = Math.atan(tanA);
// A
float Ax = (float) (mLittleCirclePoint.x + mLittleCircleRadius*Math.sin(arcTanA));
float Ay = (float) (mLittleCirclePoint.y - mLittleCircleRadius*Math.cos(arcTanA));
// B
float Bx = (float) (mBigCirclePoint.x + mBigCircleRadius*Math.sin(arcTanA));
float By = (float) (mBigCirclePoint.y - mBigCircleRadius*Math.cos(arcTanA));
// C
float Cx = (float) (mBigCirclePoint.x - mBigCircleRadius*Math.sin(arcTanA));
float Cy = (float) (mBigCirclePoint.y + mBigCircleRadius*Math.cos(arcTanA));
// D
float Dx = (float) (mLittleCirclePoint.x - mLittleCircleRadius*Math.sin(arcTanA));
float Dy = (float) (mLittleCirclePoint.y + mLittleCircleRadius*Math.cos(arcTanA));
// 拼裝 貝塞爾的曲線(xiàn)路徑
bezeierPath.moveTo(Ax,Ay); // 移動(dòng)
// 兩個(gè)點(diǎn)
PointF controlPoint = getControlPoint();
// 畫(huà)了第一條 第一個(gè)點(diǎn)(控制點(diǎn),兩個(gè)圓心的中心點(diǎn)),終點(diǎn)
bezeierPath.quadTo(controlPoint.x,controlPoint.y,Bx,By);
// 畫(huà)第二條
bezeierPath.lineTo(Cx,Cy); // 鏈接到
bezeierPath.quadTo(controlPoint.x,controlPoint.y,Dx,Dy);
bezeierPath.close();
return bezeierPath;
}
二、完善代碼
這部分我們需要完善所有代碼,實(shí)現(xiàn)代碼的分離,使得所用View都能被拖動(dòng),且需要?jiǎng)?chuàng)建一個(gè)監(jiān)聽(tīng)器來(lái)監(jiān)聽(tīng)View是否拖動(dòng)結(jié)束了,結(jié)束后調(diào)用回調(diào)方法以便需要做其他處理。
需要完成的功能:
(1)將傳入的View畫(huà)出來(lái)
(2)在手指抬起時(shí)判斷是爆炸還是回彈
(3)完成回彈和爆炸的代碼部分
(4)回彈或者爆炸結(jié)束后調(diào)用回調(diào)通知?jiǎng)赢?huà)結(jié)束
(5)使用WindowManager把自定義拖拽View加進(jìn)去,隱藏原來(lái)得View實(shí)現(xiàn)View在任意地方拖動(dòng)
完整代碼部分
(1)自定義View的代碼
public class MsgDrafitingView extends View{
private PointF mLittleCirclePoint;
private PointF mBigCirclePoint;
private Paint mPaint;
//大圓半徑
private int mBigCircleRadius = 10;
//小圓半徑
private int mLittleCircleRadiusMax = 10;
private int mLittleCircleRadiusMin = 2;
private int mLittleCircleRadius;
private Bitmap dragBitmap;
private OnToucnUpListener mOnToucnUpListener;
public MsgDrafitingView(Context context) {
this(context,null);
}
public MsgDrafitingView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public MsgDrafitingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mBigCircleRadius = dip2px(mBigCircleRadius);
mLittleCircleRadiusMax = dip2px(mLittleCircleRadiusMax);
mLittleCircleRadiusMin = dip2px(mLittleCircleRadiusMin);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setAntiAlias(true);
mPaint.setDither(true);
}
private int dip2px(int dip) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dip,getResources().getDisplayMetrics());
}
@Override
protected void onDraw(Canvas canvas) {
if (mBigCirclePoint == null || mLittleCirclePoint == null) {
return;
}
//畫(huà)大圓
canvas.drawCircle(mBigCirclePoint.x, mBigCirclePoint.y, mBigCircleRadius, mPaint);
//獲得貝塞爾路徑
Path bezeierPath = getBezeierPath();
if (bezeierPath!=null) {
// 小到一定層度就不見(jiàn)了(不畫(huà)了)
canvas.drawCircle(mLittleCirclePoint.x, mLittleCirclePoint.y, mLittleCircleRadius, mPaint);
// 畫(huà)貝塞爾曲線(xiàn)
canvas.drawPath(bezeierPath, mPaint);
}
// 畫(huà)圖片
if (dragBitmap != null) {
canvas.drawBitmap(dragBitmap, mBigCirclePoint.x - dragBitmap.getWidth() / 2,
mBigCirclePoint.y - dragBitmap.getHeight() / 2, null);
}
}
private Path getBezeierPath() {
double distance = getDistance(mBigCirclePoint,mLittleCirclePoint);
mLittleCircleRadius = (int) (mLittleCircleRadiusMax - distance / 10);
if (mLittleCircleRadius < mLittleCircleRadiusMin) {
// 超過(guò)一定距離 貝塞爾和固定圓都不要畫(huà)了
return null;
}
Path bezeierPath = new Path();
// 求角 a
// 求斜率
float dy = (mBigCirclePoint.y-mLittleCirclePoint.y);
float dx = (mBigCirclePoint.x-mLittleCirclePoint.x);
float tanA = dy/dx;
// 求角a
double arcTanA = Math.atan(tanA);
// A
float Ax = (float) (mLittleCirclePoint.x + mLittleCircleRadius*Math.sin(arcTanA));
float Ay = (float) (mLittleCirclePoint.y - mLittleCircleRadius*Math.cos(arcTanA));
// B
float Bx = (float) (mBigCirclePoint.x + mBigCircleRadius*Math.sin(arcTanA));
float By = (float) (mBigCirclePoint.y - mBigCircleRadius*Math.cos(arcTanA));
// C
float Cx = (float) (mBigCirclePoint.x - mBigCircleRadius*Math.sin(arcTanA));
float Cy = (float) (mBigCirclePoint.y + mBigCircleRadius*Math.cos(arcTanA));
// D
float Dx = (float) (mLittleCirclePoint.x - mLittleCircleRadius*Math.sin(arcTanA));
float Dy = (float) (mLittleCirclePoint.y + mLittleCircleRadius*Math.cos(arcTanA));
// 拼裝 貝塞爾的曲線(xiàn)路徑
bezeierPath.moveTo(Ax,Ay); // 移動(dòng)
// 兩個(gè)點(diǎn)
PointF controlPoint = getControlPoint();
// 畫(huà)了第一條 第一個(gè)點(diǎn)(控制點(diǎn),兩個(gè)圓心的中心點(diǎn)),終點(diǎn)
bezeierPath.quadTo(controlPoint.x,controlPoint.y,Bx,By);
// 畫(huà)第二條
bezeierPath.lineTo(Cx,Cy); // 鏈接到
bezeierPath.quadTo(controlPoint.x,controlPoint.y,Dx,Dy);
bezeierPath.close();
return bezeierPath;
}
/**
* 獲得控制點(diǎn)距離
*/
public PointF getControlPoint() {
return new PointF((mLittleCirclePoint.x+mBigCirclePoint.x)/2,(mLittleCirclePoint.y+mBigCirclePoint.y)/2);
}
/**
* 獲得兩點(diǎn)之間的距離
*/
private double getDistance(PointF point1, PointF point2) {
return Math.sqrt((point1.x - point2.x) * (point1.x - point2.x) + (point1.y - point2.y) * (point1.y - point2.y));
}
/**
* 綁定View
*/
public static void attach(View view, MsgDrafitingListener.BubbleDisappearListener disappearListener) {
view.setOnTouchListener(new MsgDrafitingListener(view.getContext(),disappearListener));
}
public void initPoint(float x, float y) {
mBigCirclePoint = new PointF(x,y);
mLittleCirclePoint = new PointF(x,y);
}
public void updatePoint(float x,float y)
{
mBigCirclePoint.x = x;
mBigCirclePoint.y = y;
invalidate();
}
public void setDragBitmap(Bitmap dragBitmap) {
this.dragBitmap = dragBitmap;
}
public void setOnToucnUpListener(OnToucnUpListener listener)
{
mOnToucnUpListener = listener;
}
public interface OnToucnUpListener {
// 還原
void restore();
// 消失爆炸
void dismiss(PointF pointF);
}
/**
* 處理手指抬起后的操作
*/
public void OnTouchUp()
{
if (mLittleCircleRadius > mLittleCircleRadiusMin) {
// 回彈 ValueAnimator 值變化的動(dòng)畫(huà) 0 變化到 1
ValueAnimator animator = ObjectAnimator.ofFloat(1);
animator.setDuration(250);
final PointF start = new PointF(mBigCirclePoint.x, mBigCirclePoint.y);
final PointF end = new PointF(mLittleCirclePoint.x, mLittleCirclePoint.y);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float percent = (float) animation.getAnimatedValue();// 0 - 1
PointF pointF = Utils.getPointByPercent(start, end, percent);
//更新位子
updatePoint(pointF.x, pointF.y);
}
});
// 設(shè)置一個(gè)差值器 在結(jié)束的時(shí)候回彈
animator.setInterpolator(new OvershootInterpolator(3f));
animator.start();
// 還要通知 TouchListener
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if(mOnToucnUpListener != null){
mOnToucnUpListener.restore();
}
}
});
} else {
// 爆炸
if(mOnToucnUpListener != null){
mOnToucnUpListener.dismiss(mBigCirclePoint);
}
}
}
}
(2)自定義OnTouchListenner的代碼
public class MsgDrafitingListener implements View.OnTouchListener {
private WindowManager mWindowManager;
private WindowManager.LayoutParams params;
private MsgDrafitingView mMsgDrafitingView;
private Context context;
// 爆炸動(dòng)畫(huà)
private FrameLayout mBombFrame;
private ImageView mBombImage;
private BubbleDisappearListener mDisappearListener;
public MsgDrafitingListener(Context context,BubbleDisappearListener disappearListener)
{
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
params = new WindowManager.LayoutParams();
mMsgDrafitingView = new MsgDrafitingView(context);
//背景透明
params.format = PixelFormat.TRANSPARENT;
this.context = context;
mBombFrame = new FrameLayout(context);
mBombImage = new ImageView(context);
mBombImage.setLayoutParams(new FrameLayout.LayoutParams(Utils.dip2px(30,context),
Utils.dip2px(30,context)));
mBombFrame.addView(mBombImage);
this.mDisappearListener = disappearListener;
}
@Override
public boolean onTouch(final View view, MotionEvent motionEvent) {
switch (motionEvent.getAction())
{
case MotionEvent.ACTION_DOWN:
//隱藏自己
view.setVisibility(View.INVISIBLE);
mWindowManager.addView(mMsgDrafitingView,params);
int[] location = new int[2];
view.getLocationOnScreen(location);
Bitmap bitmap = getBitmapByView(view);
//y軸需要減去狀態(tài)欄的高度
mMsgDrafitingView.initPoint(location[0] + view.getWidth() / 2,
location[1]+view.getHeight()/2 -Utils.getStatusBarHeight(context));
// 給消息拖拽設(shè)置一個(gè)Bitmap
mMsgDrafitingView.setDragBitmap(bitmap);
//設(shè)置OnTouchUpListener
mMsgDrafitingView.setOnToucnUpListener(new MsgDrafitingView.OnToucnUpListener() {
@Override
public void restore() {
//還原位子
// 把消息的View移除
mWindowManager.removeView(mMsgDrafitingView);
// 把原來(lái)的View顯示
view.setVisibility(View.VISIBLE);
}
@Override
public void dismiss(PointF pointF) {
//爆炸效果
// 要去執(zhí)行爆炸動(dòng)畫(huà) (幀動(dòng)畫(huà))
//移除拖拽的view
mWindowManager.removeView(mMsgDrafitingView);
// 要在 mWindowManager 添加一個(gè)爆炸動(dòng)畫(huà)
mWindowManager.addView(mBombFrame,params);
mBombImage.setBackgroundResource(R.drawable.anim_bubble_pop);
AnimationDrawable drawable = (AnimationDrawable) mBombImage.getBackground();
mBombImage.setX(pointF.x-drawable.getIntrinsicWidth()/2);
mBombImage.setY(pointF.y-drawable.getIntrinsicHeight()/2);
drawable.start();
// 等它執(zhí)行完之后我要移除掉這個(gè) 爆炸動(dòng)畫(huà)也就是 mBombFrame
mBombImage.postDelayed(new Runnable() {
@Override
public void run() {
mWindowManager.removeView(mBombFrame);
// 通知一下外面該消失
if(mDisappearListener != null){
mDisappearListener.dismiss(view);
}
}
},getAnimationDrawableTime(drawable));
}
});
break;
case MotionEvent.ACTION_MOVE:
mMsgDrafitingView.updatePoint(motionEvent.getRawX(),
motionEvent.getRawY() - Utils.getStatusBarHeight(context));
break;
case MotionEvent.ACTION_UP:
mMsgDrafitingView.OnTouchUp();
break;
}
return true;
}
private Bitmap getBitmapByView(View view) {
view.buildDrawingCache();
Bitmap bitmap = view.getDrawingCache();
return bitmap;
}
public interface BubbleDisappearListener {
void dismiss(View view);
}
/**
* 獲取爆炸動(dòng)畫(huà)畫(huà)的時(shí)間
* @param drawable
* @return
*/
private long getAnimationDrawableTime(AnimationDrawable drawable) {
int numberOfFrames = drawable.getNumberOfFrames();
long time = 0;
for (int i=0;i<numberOfFrames;i++){
time += drawable.getDuration(i);
}
return time;
}
}
(3)View的調(diào)用代碼
public class MsgDrafitingViewActivity extends AppCompatActivity{
private Button mButton;
private TextView mText;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.qq_msg_drafitingview_activity);
mButton = findViewById(R.id.mBtn);
mText = findViewById(R.id.mText);
MsgDrafitingView.attach(mButton, new MsgDrafitingListener.BubbleDisappearListener() {
@Override
public void dismiss(View view) {
}
});
MsgDrafitingView.attach(mText, new MsgDrafitingListener.BubbleDisappearListener() {
@Override
public void dismiss(View view) {
}
});
}
}
源碼地址:源碼Github地址
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android把商品添加到購(gòu)物車(chē)的動(dòng)畫(huà)效果(貝塞爾曲線(xiàn))
- Android中貝塞爾曲線(xiàn)的繪制方法示例代碼
- android中貝塞爾曲線(xiàn)的應(yīng)用示例
- Android 利用三階貝塞爾曲線(xiàn)繪制運(yùn)動(dòng)軌跡的示例
- Android貝塞爾曲線(xiàn)初步學(xué)習(xí)第三課 Android實(shí)現(xiàn)添加至購(gòu)物車(chē)的運(yùn)動(dòng)軌跡
- Android貝塞爾曲線(xiàn)初步學(xué)習(xí)第一課
- Android貝塞爾曲線(xiàn)實(shí)現(xiàn)手指軌跡
- Android貝塞爾曲線(xiàn)實(shí)現(xiàn)填充不規(guī)則圖形并隨手指運(yùn)動(dòng)
- Android使用貝塞爾曲線(xiàn)仿QQ聊天消息氣泡拖拽效果
- android貝塞爾曲線(xiàn)實(shí)現(xiàn)波浪效果
相關(guān)文章
Android自定義TitleView標(biāo)題開(kāi)發(fā)實(shí)例
這篇文章主要介紹了Android自定義TitleView標(biāo)題開(kāi)發(fā)實(shí)例的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-09-09
android PopupWindow 和 Activity彈出窗口實(shí)現(xiàn)方式
本人小菜一個(gè)。目前只見(jiàn)過(guò)兩種彈出框的實(shí)現(xiàn)方式,第一種是最常見(jiàn)的PopupWindow,第二種也就是Activity的方式是前幾天才見(jiàn)識(shí)過(guò),需要的朋友可以參考下2012-11-11
Android設(shè)置鈴聲實(shí)現(xiàn)代碼
這篇文章主要介紹了Android設(shè)置鈴聲實(shí)現(xiàn)代碼,以實(shí)例形式分析了Android中鈴聲設(shè)置的相關(guān)技巧,非常簡(jiǎn)單實(shí)用,需要的朋友可以參考下2015-10-10
Android 點(diǎn)擊生成二維碼功能實(shí)現(xiàn)代碼
二維碼,我們也稱(chēng)作QRCode,QR表示quick response即快速響應(yīng),在很多App中我們都能見(jiàn)到二維碼的身影,最常見(jiàn)的莫過(guò)于微信了。接下來(lái)給大家介紹android 點(diǎn)擊生成二維碼功能實(shí)現(xiàn)代碼,需要的朋友參考下吧2017-11-11
Android 日期和時(shí)間的使用實(shí)例詳解
這篇文章主要介紹了Android 日期和時(shí)間的使用實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2016-12-12
Android room數(shù)據(jù)庫(kù)使用詳解
這篇文章主要介紹了Android room數(shù)據(jù)庫(kù)使用,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-11-11

