最近在複習Android,重新撿起自定義控件。
總結一句話,不要用小聰明規避問題,不然你下次遇到了,你還是什麼都不懂。
先看效果:
工程以及說明: QQ小紅點
首先,看到這個,想到的就是使用貝塞爾曲線了。如果你對怎麼繪製不瞭解,可以先通過啓艦大佬,瞭解QQ小紅點怎麼實現的。
自定義控件三部曲之繪圖篇(十五)——QQ紅點拖動刪除效果實現(基本原理篇)
不過上面的文章,只能在view級別上使用,就算使用了 clipchild = false,也無法誇層級繪製的,所以這篇文章是在此基礎上,讓它可以在所有view上都能使用。
思路如下:
- 用一個BezierPointView繼承TextView,拿到自定義配置參數,比如大小,顏色
- 在 BezierPointView 的Down事件時,把QQ小紅點 (BezierPointWindow) 通過 Windowmanager 添加進來,再把 BezierPointView 的visiable 設置 gone
- 當 BezierPointWindow 的Up 事件時,把座標傳遞回來,然後移除BezierPointWindow,添加一個ImageView,再在此做自定義的屬性動畫,或者默認的圖片爆炸效果
- 移除windowmanager,讓 BezierPointView 顯示出來即可。
這裏我沒有跟啓艦大佬那樣,用viewgroup來實現,其實用自定義 View 即可實現。實現的原理,啓艦大佬已經講解得很清楚了
我們只要找準一個方向,計算切線即可。如果兩個 a 相等呢?相信你的判斷兩條線是否平行的原理還記得。
既然知道了兩個小球的座標和半徑,那麼切點的座標也很好理解了,然後再計算不斷變化的半徑和最大斷開的長度即可。
/**
* 計算貝塞爾曲線
* 由於Math 的掃腳函數,都自帶正負的,所以,只需要計算一種方向即可
* 這裏的計算方向爲右下
*/
private void calculateBeizer() {
float x0 = mStartPoint.x;
float y0 = mStartPoint.y;
float x = mMovePoint.x;
float y = mMovePoint.y;
//算出夾角
float dx = x - x0;
float dy = y - y0;
double a = Math.atan(dy / dx);
//拿到圓切點的長度偏移量
float offsetx0 = (float) (mDrawRadius * Math.sin(a));
float offsety0 = (float) (mDrawRadius * Math.cos(a));
//拿到第二個小球的偏移量
float offsetx = (float) (mDefaultRadius * Math.sin(a));
float offsety = (float) (mDefaultRadius * Math.cos(a));
//算出第一個圓的切點座標
float p0x = x0 + offsetx0;
float p0y = y0 - offsety0;
float p1x = x0 - offsetx0;
float p1y = y0 + offsety0;
//算出第二個圓的切點座標
float p2x = x + offsetx;
float p2y = y - offsety;
float p3x = x - offsetx;
float p3y = y + offsety;
//計算貝塞爾輔助點
float anchorx = (x0 + x) / 2;
float anchory = (y0 + y) / 2;
//清掉上次,避免殘留
mPath.reset();
//形成貝塞爾曲線
mPath.moveTo(p0x, p0y);
mPath.quadTo(anchorx, anchory, p2x, p2y);
mPath.lineTo(p3x, p3y);
mPath.quadTo(anchorx, anchory, p1x, p1y);
mPath.close();
//超過一定距離時,且圓形的半徑也要跟着變小
double distance = getDistance(mMovePoint, mStartPoint);
mDrawRadius = (int) (mDefaultRadius - distance / 12);
if (mDrawRadius <= 7) {
mDrawRadius = 7;
}
if (distance >= mMaxMoveLength) {
// 超過一定距離 貝塞爾和固定圓都不要畫了
mIsBreakUp = true;
mDrawRadius = 0;
mPath.reset();
return;
}
}
在所有 View 上使用
我們可以通過一個 BezierPointView 拿到自定義屬性之後,在 onSizeChange 的時候,配置windowmanger:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mPointHelper = new PointHelper(this);
setOnTouchListener(mPointHelper);
}
而PointHelper 其實是一個 windowmanager 的工具類,繼承View.OnTouchListener,這樣就可以在 onTouch 方法中,添加windowmanager :
@Override
public boolean onTouch(View v, MotionEvent event) {
ViewParent parent = v.getParent();
if (parent == null) {
return false;
}
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
if (!mWindowView.isCanMove()) {
return false;
}
//接管父控件的touch事件
parent.requestDisallowInterceptTouchEvent(true);
/**
* 當按下時,再把View添加到windowmanager,此時的windowmanager是全屏的;
* 所以,pointview的操作不會因爲控件大小的限制,導致繪製不全的問題
*/
addPointToWindow();
}
return mWindowView.onTouchEvent(event);
可以看到 touch之後是交給 BezierPointWindow 的touch 事件,而addPointToWindow 爲具體的添加效果。
/**
* 把TextView 的緩存bitmap給BezierPointWindow繪製
*/
private void addPointToWindow() {
if (mContainer == null) {
mContainer = new FrameLayout(mContext);
mContainer.setClipChildren(false);
mContainer.setClipToPadding(false);
mWindowView.setLayoutParams(mLayoutParams);
}
mContainer.removeAllViews();
mContainer.addView(mWindowView, mParams);
mWindowManager.addView(mContainer, mParams);
//初始化座標
mPointView.getLocationInWindow(mPos);
int width = mPointView.getWidth();
int height = mPointView.getHeight();
//設置大小和起始位置
mWindowView.initPoint(mPos[0] + width / 2, mPos[1] + height / 2);
mWindowView.setPointListener(this);
mWindowView.setVisibility(View.VISIBLE);
//拿到bitmap
mPointView.setDrawingCacheEnabled(true);
//拿到TextView的緩存bitmap,給BezierPointWindow 繪製,也爲後面up的 imageview 當做背景圖
Bitmap bitmap = Bitmap.createBitmap(mPointView.getDrawingCache());
mPointView.setDrawingCacheEnabled(false);
mWindowView.setBitmap(bitmap);
}
爲了避免閃爍問題,我們需要當 BezierPointWindow 繪製好 bitmap 之後,再讓BezierPointView 消失:
@Override
public void onDrawReady() {
//這個時候,可以消失了,避免閃爍
mPointView.setVisibility(View.GONE);
}
而在 up 之後,之後,只需要添加一個 ImageView,實現要消失的動畫即可:
@Override
public void destroy(PointF pointF) {
//移除所有
mContainer.removeAllViews();
//添加imageview,用於動畫
mContainer.addView(mImageView, mLayoutParams);
//指定初始位置
mImageView.setX(pointF.x - mPointView.getWidth() * 1.0f / 2);
mImageView.setY(pointF.y - mPointView.getHeight() * 1.0f / 2);
if (mPointView.isUseSelfAnim()){
//使用自定義動畫
mImageView.setImageBitmap(mWindowView.getBitmap());
if (mPointView.getListener() != null) {
mPointView.getListener().destory(mImageView);
}
}else{
//使用佈局標籤的屬性動畫
if (mPointView.getAnimatorRes() != -1){
mImageView.setImageBitmap(mWindowView.getBitmap());
mAnimator = AnimatorInflater.loadAnimator(mContext, mPointView.getAnimatorRes());
mAnimator.setTarget(mImageView);
mAnimator.start();
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
removeView();
}
});
}else{
//使用默認動畫,即圖片爆炸
mImageView.setBackgroundResource(R.drawable.anim_blow);
mAnimationDrawable = (AnimationDrawable) mImageView.getBackground();
mAnimationDrawable.start();
mImageView.postDelayed(new Runnable() {
@Override
public void run() {
removeView();
}
},getAnimationDrawableTime(mAnimationDrawable));
}
}
}
需要注意的是,最後需要使用 removeView 方法,不然這層 windowmanager 的view 不會消失:
/**
* 移除View,這個必須要實現,否則就有一層view在頂層,啥也操作不了
*/
public void removeView() {
if (mWindowManager != null) {
if (mContainer != null && mContainer.isAttachedToWindow()) {
mWindowManager.removeView(mContainer);
}
}
//還原set屬性,防止view被設置動畫,初始位置錯亂
mWindowView.clearAnimation();
mWindowView.setAlpha(1);
mWindowView.setX(0);
mWindowView.setY(0);
mWindowView.setScaleX(1);
mWindowView.setScaleY(1);
mWindowView.setVisibility(View.GONE);
if (mContainer != null && mContainer.getChildCount() > 0) {
mContainer.removeView(mWindowView);
}
}