自定义控件:QQ气泡效果粘性控件的实现
學習目的
- 了解幾何圖形工具的用法
- 掌握畫不規則圖形的方法
應用場景:未讀提醒,效果圖:
繪制一幀的效果
畫一幀粘性控件的步驟分析
將中間連接部分水平放置,四個角的坐標定為固定值,分別標記上點的編號,矩形中心的點為控件點,畫曲線時用
自定義一個GooView 繼承View
public class GooView extends View {private Paint paint;public GooView(Context context) {this(context,null);}public GooView(Context context, AttributeSet attrs) {this(context, attrs,0);}public GooView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);//初始化畫筆paint = new Paint();paint.setAntiAlias(true);paint.setColor(Color.RED);}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);//畫中間連接部分Path path = new Path();//跳到點1,默認為(0f,0f)path.moveTo(250f, 250f);//從點1->點2 畫曲線path.quadTo(150f, 300f, 50f, 250f);//從點2->點3 畫直線path.lineTo(50f, 350f);//從點3->點4 畫曲線path.quadTo(150f, 300f, 250f, 350f);canvas.drawPath(path, paint);//畫拖拽圓canvas.drawCircle(90f, 90f, 16f, paint);//畫固定圓canvas.drawCircle(150f, 150f, 12f, paint);} }第20-30 行用Path 畫中間曲線部分
第25 行quadTo(x1,y1,x2,y2)方法可以畫當前所在點到x2,y2 間的一條曲線,x1,y1 是當前點與x2,y2 間的一個控件點,它的位置決定曲線彎曲的方向和弧度,將GooView 顯示到MainActivity 中
貝塞爾曲線
上述代碼調用path.quadTo()畫曲線,這種曲線叫貝塞爾曲線,有一個起點和終點,還可以有2個或3個控制點,其中控制點是控制曲線的彎曲形狀,控制點不同,曲線的彎曲形狀就不同。
二階貝塞爾曲線,三階貝塞爾曲線
替換變量
分別給拖拽圓,固定圓的圓心,半徑,兩個附著點命名,修改GooView 的onDraw()方法
protected void onDraw(Canvas canvas) {super.onDraw(canvas);//固定圓的兩個附著點PointF[] mStickPoints = new PointF[]{new PointF(250f, 250f),new PointF(250f, 350f)};//固定圓的兩個附著點PointF[] mDragPoints = new PointF[]{new PointF(50f, 250f),new PointF(50f, 350f)};//控制點PointF mControlPoint = new PointF(150f, 300f);//畫中間連接部分Path path = new Path();//跳到點1,默認為(0f,0f)path.moveTo(mStickPoints[0].x, mStickPoints[0].y);//從點1->點2 畫曲線path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y);//從點2->點3 畫直線path.lineTo( mDragPoints[1].x, mDragPoints[1].y);//從點3->點4 畫曲線path.quadTo(mControlPoint.x, mControlPoint.y, mStickPoints[1].x, mStickPoints[1].y);canvas.drawPath(path, paint);//畫拖拽圓//拖拽圓圓心PointF mDragCenter = new PointF(90f, 90f);//拖拽圓半徑float mDragRadius = 16f;canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, paint);//畫固定圓//固定圓圓心PointF mStickCenter = new PointF(150f, 150f);//固定圓半徑float mStickRadius = 12f;canvas.drawCircle(mStickCenter.x, mStickCenter.y, mStickRadius, paint); }第3-14 行替換附著點及控制點
第30-40 行替換拖拽圓及固定圓的圓心及半徑
將替換后的變量轉換成GooView 的成員變量
計算變量
拖拽圓和固定圓的圓心和半徑已知,角3 的正弦值為兩圓心縱坐標之差比上橫坐標之差,則角3 的角度可知,則角1 可知,AB,AC 的長度即可計算出來,mDragPoints[0]的坐標可以計算出來,同理其它三個附著點坐標也可知。mControlPoint 為兩圓心連線的中點
幾何圖形工具
/*** 幾何圖形工具*/ public class GeometryUtil {/*** As meaning of method name.* 獲得兩點之間的距離* @param p0* @param p1* @return*/public static float getDistanceBetween2Points(PointF p0, PointF p1) {float distance = (float) Math.sqrt(Math.pow(p0.y - p1.y, 2) +Math.pow(p0.x - p1.x, 2));return distance;}/*** Get middle point between p1 and p2.* 獲得兩點連線的中點* @param p1* @param p2* @return*/public static PointF getMiddlePoint(PointF p1, PointF p2) {return new PointF((p1.x + p2.x) / 2.0f, (p1.y + p2.y) / 2.0f);}/*** Get point between p1 and p2 by percent.* 根據百分比獲取兩點之間的某個點坐標* @param p1* @param p2* @param percent* @return*/public static PointF getPointByPercent(PointF p1, PointF p2, float percent) {return new PointF(evaluateValue(percent, p1.x , p2.x),evaluateValue(percent, p1.y , p2.y));}/*** 根據分度值,計算從start 到end 中,fraction 位置的值。fraction 范圍為0 -> 1* @param fraction* @param start* @param end* @return*/public static float evaluateValue(float fraction, Number start, Number end){return start.floatValue() + (end.floatValue() - start.floatValue()) * fraction;}/*** Get the point of intersection between circle and line.* 獲取通過指定圓心,斜率為lineK 的直線與圓的交點。** @param pMiddle The circle center point.* @param radius The circle radius.* @param lineK The slope of line which cross the pMiddle.* @return*/public static PointF[] getIntersectionPoints(PointF pMiddle, float radius, DoublelineK) {PointF[] points = new PointF[2];float radian, xOffset = 0, yOffset = 0;if(lineK != null){radian= (float) Math.atan(lineK);xOffset = (float) (Math.sin(radian) * radius);yOffset = (float) (Math.cos(radian) * radius);}else {xOffset = radius;yOffset = 0;}points[0] = new PointF(pMiddle.x + xOffset, pMiddle.y - yOffset);points[1] = new PointF(pMiddle.x - xOffset, pMiddle.y + yOffset);return points;} }利用幾何圖形工具類計算四個附著點坐標及控件點坐標
protected void onDraw(Canvas canvas) {super.onDraw(canvas);float yOffset = mStickCenter.y - mDragCenter.y;float xOffset = mStickCenter.x - mDragCenter.x;Double lineK = null;if(xOffset != 0){//xOffset 分母不能為0lineK = (double) (yOffset/xOffset);}//計算四個附著點mDragPoints = GeometryUtil.getIntersectionPoints(mDragCenter,mDragRadius, lineK);mStickPoints = GeometryUtil.getIntersectionPoints(mStickCenter,mStickRadius, lineK);//一個控制點mControlPoint = GeometryUtil.getMiddlePoint(mDragCenter, mStickCenter);// 畫中間連接部分Path path = new Path();// 跳到點1,默認為(0f,0f)path.moveTo(mStickPoints[0].x, mStickPoints[0].y);// 從點1->點2 畫曲線path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x,mDragPoints[0].y);// 從點2->點3 畫直線path.lineTo(mDragPoints[1].x, mDragPoints[1].y);// 從點3->點4 畫曲線path.quadTo(mControlPoint.x, mControlPoint.y, mStickPoints[1].x,mStickPoints[1].y);canvas.drawPath(path, paint);// 畫拖拽圓canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, paint);// 畫固定圓canvas.drawCircle(mStickCenter.x, mStickCenter.y, mStickRadius, paint); }第3-17 行計算四個附著點及控制點坐標
1.4 計算固定圓半徑
GooView 重寫onSizeChanged()方法,計算狀態欄高度
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);//獲取狀態欄的高度,傳入一個顯示在屏幕上的view 即可statusBarHeight = Utils.getStatusBarHeight(this); }Utils.java
public class Utils {public static Toast mToast;public static void showToast(Context mContext, String msg) {if (mToast == null) {mToast = Toast.makeText(mContext, "", Toast.LENGTH_SHORT);}mToast.setText(msg);mToast.show();}/*** 獲取狀態欄高度** @param v* @return*/public static int getStatusBarHeight(View v) {if (v == null) {return 0;}Rect frame = new Rect();v.getWindowVisibleDisplayFrame(frame);return frame.top;} }修改onDraw()方法
protected void onDraw(Canvas canvas) {super.onDraw(canvas);float yOffset = mStickCenter.y - mDragCenter.y;float xOffset = mStickCenter.x - mDragCenter.x;Double lineK = null;if(xOffset != 0){//xOffset 分母不能為0lineK = (double) (yOffset/xOffset);}//計算四個附著點mDragPoints = GeometryUtil.getIntersectionPoints(mDragCenter,mDragRadius, lineK);mStickPoints = GeometryUtil.getIntersectionPoints(mStickCenter,mStickRadius, lineK);//一個控制點mControlPoint = GeometryUtil.getMiddlePoint(mDragCenter, mStickCenter);//移動畫布canvas.save();canvas.translate(0, -statusBarHeight);// 畫中間連接部分Path path = new Path();// 跳到點1,默認為(0f,0f)path.moveTo(mStickPoints[0].x, mStickPoints[0].y);// 從點1->點2 畫曲線path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x,mDragPoints[0].y);// 從點2->點3 畫直線path.lineTo(mDragPoints[1].x, mDragPoints[1].y);// 從點3->點4 畫曲線path.quadTo(mControlPoint.x, mControlPoint.y, mStickPoints[1].x,mStickPoints[1].y);canvas.drawPath(path, paint);// 畫拖拽圓canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, paint);// 畫固定圓canvas.drawCircle(mStickCenter.x, mStickCenter.y, mStickRadius, paint);canvas.restore(); }第18-20 行把畫布向上移動狀態欄的高度,移動前需要保存一下當前狀態,做完操作后需要恢復一下狀態,由于在onTouchEvent()中用的是getRawX(),getRawY()獲取的是相對屏幕的坐標,所以GooView畫圖操作時需要向上移到一個狀態欄的高度才能剛好和手指重合拖拽圓跟隨手指移動時,隨著拖拽與固定圓的距離的變大,固定圓的半徑越來越小
//允許的最大距離 float farestDistance = 80f; /*** 通過兩圓圓心的距離,計算固定圓的半徑* @return*/ private float computeStickRadius() {//通過幾何圖形工具類可以計算出兩圓圓心的距離,distance 是可以大于80f;float distance = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter);//需要的是0.0f -> 1.0f 的值,所在大于80f 讓distance 等于80fdistance = Math.min(farestDistance, distance);float percent = distance/farestDistance;//需要固定圓心半徑在12f -> 3f 間變化,可以利用類型估值器return evaluate(percent, mStickRadius, mStickRadius*0.25f); } //FloatEvaluator.java 中拷貝 public Float evaluate(float fraction, Number startValue, Number endValue) {float startFloat = startValue.floatValue();return startFloat + fraction * (endValue.floatValue() - startFloat); } protected void onDraw(Canvas canvas) {super.onDraw(canvas);//通過兩圓圓心的距離,計算固定圓的半徑float tempStickRadius = computeStickRadius();float yOffset = mStickCenter.y - mDragCenter.y;float xOffset = mStickCenter.x - mDragCenter.x;Double lineK = null;if(xOffset != 0){lineK = (double) (yOffset/xOffset);}//計算四個附著點mDragPoints = GeometryUtil.getIntersectionPoints(mDragCenter, mDragRadius,lineK);mStickPoints = GeometryUtil.getIntersectionPoints(mStickCenter, tempStickRadius,lineK);//一個控制點mControlPoint = GeometryUtil.getMiddlePoint(mDragCenter, mStickCenter);//移動畫布canvas.save();canvas.translate(0, -statusBarHeight);// 畫中間連接部分Path path = new Path();// 跳到點1,默認為(0f,0f)path.moveTo(mStickPoints[0].x, mStickPoints[0].y);// 從點1->點2 畫曲線path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x,mDragPoints[0].y);// 從點2->點3 畫直線path.lineTo(mDragPoints[1].x, mDragPoints[1].y);// 從點3->點4 畫曲線path.quadTo(mControlPoint.x, mControlPoint.y, mStickPoints[1].x,mStickPoints[1].y);canvas.drawPath(path, paint);// 畫拖拽圓canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, paint);// 畫固定圓canvas.drawCircle(mStickCenter.x, mStickCenter.y, tempStickRadius, paint);canvas.restore(); }第2 行定義最大的拖拽距離為80f
第7-24 行拖拽圓與固定圓的距離大于80f 時,取80f,通過兩圓圓心的距離與80f 相對可以求出一個0.0f
到1.0f 的值,再通過估值器可以獲得固定圓的半徑在mStickRadius,mStickRadius*0.25f 間的變化值
第27-28 行通過兩圓圓心的距離計算固定圓的半徑tempStickRadius
第39,67 行將mStickRadius 替換成計算出來的半徑tempStickRadius
事件處理
事件處理的分析
事件處理的實現
修改onTouchEvent()方法
//是否已經消失 private boolean isDisappear = false; //是否超出范圍 private boolean isOutOfRange = false; public boolean onTouchEvent(MotionEvent event) {float x;float y;switch (event.getAction()) {case MotionEvent.ACTION_DOWN://重置變量isDisappear = false;isOutOfRange = false;x = event.getRawX();y = event.getRawY();updateDragCenter(x, y);break;case MotionEvent.ACTION_MOVE:x = event.getRawX();y = event.getRawY();updateDragCenter(x, y);float d = GeometryUtil.getDistanceBetween2Points(mDragCenter,mStickCenter);// 超出范圍斷開if (d > farestDistance) {isOutOfRange = true;invalidate();}break;case MotionEvent.ACTION_UP:if (isOutOfRange) {// 剛剛超出了范圍float dis = GeometryUtil.getDistanceBetween2Points(mDragCenter,mStickCenter);if (dis > farestDistance) {// 超出范圍,松手,斷開,消失isDisappear = true;invalidate();} else {// 超出范圍,斷開,又放回去了,恢復updateDragCenter(mStickCenter.x, mStickCenter.y);}} else {// 沒有超出范圍,松手,回彈,恢復final PointF startP = new PointF(mDragCenter.x, mDragCenter.y);ValueAnimator animator = ValueAnimator.ofFloat(1.0f);animator.setDuration(500);// 插值器,回彈效果animator.setInterpolator(new OvershootInterpolator(4));animator.addUpdateListener(new AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {// 生成0.0f ->1.0f 間的值float percent = animation.getAnimatedFraction();// 計算從開始點startP 到mStickCenter 間的所有值PointF p = GeometryUtil.getPointByPercent(startP,mStickCenter, percent);updateDragCenter(p.x, p.y);}});animator.start();}break;default:break;}return true; }第1-2 行創建兩個布爾變量記錄是否已經消失及是否超出范圍
第11-12 行手指重新按下時,重置變量
第21-27 行拖拽過程中記錄是否超出范圍
第32-38 行超出范圍,松手,消失,標記當前為消失狀態
第39-41 行超出范圍,又放回去了,需要恢復,直接更新拖拽圓圓心為固定圓心即可
第45-62 行沒有超出范圍,松手,需要回彈動畫,恢復
修改onDraw()方法
protected void onDraw(Canvas canvas) {super.onDraw(canvas);// 通過兩圓圓心的距離,計算固定圓的半徑float tempStickRadius = computeStickRadius();float yOffset = mStickCenter.y - mDragCenter.y;float xOffset = mStickCenter.x - mDragCenter.x;Double lineK = null;if (xOffset != 0) {lineK = (double) (yOffset / xOffset);}// 計算四個附著點mDragPoints = GeometryUtil.getIntersectionPoints(mDragCenter,mDragRadius, lineK);mStickPoints = GeometryUtil.getIntersectionPoints(mStickCenter,tempStickRadius, lineK);// 一個控制點mControlPoint = GeometryUtil.getMiddlePoint(mDragCenter, mStickCenter);// 移動畫布canvas.save();canvas.translate(0, -statusBarHeight);// 畫出最大范圍(參考)// 只畫邊線paint.setStyle(Style.STROKE);canvas.drawCircle(mStickCenter.x, mStickCenter.y, farestDistance, paint);// 填充paint.setStyle(Style.FILL);if(!isDisappear){//沒有消失時,才繪制內容if (!isOutOfRange) {//沒有超出范圍時,才畫連接部分和固定圓// 畫中間連接部分Path path = new Path();// 跳到點1,默認為(0f,0f)path.moveTo(mStickPoints[0].x, mStickPoints[0].y);// 從點1->點2 畫曲線path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x,mDragPoints[0].y);// 從點2->點3 畫直線path.lineTo(mDragPoints[1].x, mDragPoints[1].y);// 從點3->點4 畫曲線path.quadTo(mControlPoint.x, mControlPoint.y,mStickPoints[1].x, mStickPoints[1].y);canvas.drawPath(path, paint);// 畫固定圓canvas.drawCircle(mStickCenter.x, mStickCenter.y,tempStickRadius, paint);}// 畫拖拽圓canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, paint);}canvas.restore(); }第31-54 行沒有消失時,才繪制內容,沒有超出范圍時,才繪制連接部分及固定圓
事件的監聽回調
定義監聽接口
private OnUpdateListener onUpdateListener; public OnUpdateListener getOnUpdateListener() {return onUpdateListener; } public void setOnUpdateListener(OnUpdateListener onUpdateListener) {this.onUpdateListener = onUpdateListener; } public interface OnUpdateListener{//消失時回調public void onDisappear();//恢復時回調,分為超出范圍恢復及沒有超出范圍恢復public void onReset(boolean isOutOfRange); }修改onTouchEvent()方法
public boolean onTouchEvent(MotionEvent event) {float x;float y;switch (event.getAction()) {case MotionEvent.ACTION_DOWN://重置變量isDisappear = false;isOutOfRange = false;x = event.getRawX();y = event.getRawY();updateDragCenter(x, y);break;case MotionEvent.ACTION_MOVE:x = event.getRawX();y = event.getRawY();updateDragCenter(x, y);float d = GeometryUtil.getDistanceBetween2Points(mDragCenter,mStickCenter);// 超出范圍斷開if (d > farestDistance) {isOutOfRange = true;invalidate();}break;case MotionEvent.ACTION_UP:if (isOutOfRange) {// 剛剛超出了范圍float dis = GeometryUtil.getDistanceBetween2Points(mDragCenter,mStickCenter);if (dis > farestDistance) {// 超出范圍,松手,斷開,消失isDisappear = true;invalidate();if(onUpdateListener != null){onUpdateListener.onDisappear();}} else {// 超出范圍,斷開,又放回去了,恢復updateDragCenter(mStickCenter.x, mStickCenter.y);if(onUpdateListener != null){onUpdateListener.onReset(true);}}} else {// 沒有超出范圍,松手,回彈,恢復final PointF startP = new PointF(mDragCenter.x, mDragCenter.y);ValueAnimator animator = ValueAnimator.ofFloat(1.0f);animator.setDuration(500);// 插值器,回彈效果animator.setInterpolator(new OvershootInterpolator(4));animator.addUpdateListener(new AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {// 生成0.0f ->1.0f 間的值float percent = animation.getAnimatedFraction();// 計算從開始點startP 到mStickCenter 間的所有值PointF p = GeometryUtil.getPointByPercent(startP,mStickCenter, percent);updateDragCenter(p.x, p.y);}});animator.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationEnd(Animator animation) {super.onAnimationEnd(animation);//需要在動畫結束時調用if(onUpdateListener != null){onUpdateListener.onReset(false);}}});animator.start();}break;default:break;}return true; }第35-37 行標記消失時,回調onDisappear()方法
第41-42 行恢復時回調onReset()方法,此時超出過范圍,所以參數傳入true
第64-73 行添加動畫監聽,在動畫結束時回調onReset()方法,此時沒有超出范圍,所以參數傳入false
修改MainActivity 測試監聽回調
public class MainActivity extends Activity {@Overrideprotected void onCreate(Bundle savedInstanceState) {requestWindowFeature(Window.FEATURE_NO_TITLE);super.onCreate(savedInstanceState);GooView view = new GooView(this);setContentView(view);view.setOnUpdateListener(new OnUpdateListener() {@Overridepublic void onReset(boolean isOutOfRange) {Utils.showToast(getApplicationContext(), "onReset:"+isOutOfRange);}@Overridepublic void onDisappear() {Utils.showToast(getApplicationContext(), "onDisappear");}});} }RecyclerView的處理
如效果圖看到的紅色圓形控件為TextView并不是我們的GooView
item布局文件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="60dp"><TextView android:text="這是標題"android:layout_width="0dp"android:layout_weight="1"android:textSize="30sp"android:layout_height="wrap_content"android:id="@+id/tv_title"/> <!-- 此處是一個TextView加入一個圓形背景,并不是GooView 原因:RecyclerView的條目顯示區域僅僅有一塊,而GooView的顯示區域需要整個屏幕,如果直接將GooView放在條目中,拖動后會影響GooView的顯示,故:使用TextView來顯示,GooView后期動態加入 --><TextView android:id="@+id/tv_unReadMsgCount"android:layout_width="30dp"android:layout_height="30dp"android:text="6"android:textColor="#fff"android:textSize="23sp"android:layout_alignParentRight="true"android:layout_centerVertical="true"android:gravity="center"android:layout_marginRight="10dp"android:background="@drawable/tv_showmsg_shape"/> </LinearLayout>圓形背景tv_showmsg_shape
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"><solid android:color="#f00"/> </shape>處理RecyclerView
@Override protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main1);List<Msg> msgList=new ArrayList<>();for (int i = 0; i < 50; i++) {msgList.add(new Msg("標題"+i,i));}RecyclerView rlv= (RecyclerView) findViewById(R.id.rlv);rlv.setLayoutManager(new LinearLayoutManager(this));rlv.setAdapter(new MsgAdapter(msgList)); }//適配器處理 public class MsgAdapter extends Adapter<MsgAdapter.MyViewHolder> {private List<Msg> msgList;public MsgAdapter(List<Msg> msgList) {this.msgList = msgList;}@Overridepublic MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.rlv_item, parent,false);return new MyViewHolder(view);}@Overridepublic void onBindViewHolder(MyViewHolder holder, int position) {holder.tv_title.setText(msgList.get(position).title);//判斷當未讀消息數等于0,則隱藏對應的TextView控件int unReadMsgCount = msgList.get(position).unReadMsgCount;if (unReadMsgCount == 0) {holder.tv_unReadMsgCount.setVisibility(View.INVISIBLE);} else {holder.tv_unReadMsgCount.setVisibility(View.VISIBLE);holder.tv_unReadMsgCount.setText(unReadMsgCount+"");}}@Overridepublic int getItemCount() {return msgList.size();}public static class MyViewHolder extends RecyclerView.ViewHolder {public TextView tv_title;public TextView tv_unReadMsgCount;public MyViewHolder(View itemView) {super(itemView);tv_title = (TextView) itemView.findViewById(R.id.tv_title);tv_unReadMsgCount = (TextView) itemView.findViewById(R.id.tv_unReadMsgCount);}} }加入RecyclerView后的事件分發問題(事件分發機制)
加入GooView后的處理
實現效果的原理:當用戶觸摸到右側的圓形背景TextView的時候,讓TextView隱藏,利用WindowManager添加GooView當松開手后,將GooView移除,讓TextView顯示
GooView的準備工作
讓GooView能夠顯示文本,定義為GooView設置文本的方法
private String GooViewText=""; public void setGooViewText(String gooViewText) {GooViewText = gooViewText; }在onDraw方法中繪制文本
@Override protected void onDraw(Canvas canvas) {if (!isDisappear){if (!isOutOfRange){...//繪制文本 注意:要先繪制拖拽圓,再繪制文本,否則會被蓋住drawGooViewText(canvas);...}//繪制拖拽圓canvas.drawCircle(dragCenter.x,dragCenter.y,dragRadius,paint);... } } private void drawGooViewText(Canvas canvas) {paint.setColor(Color.WHITE);//在android中任何看到的視圖都是矩形的//計算文本的寬高:原理是將文本外套上一個矩形,矩形的寬高就是文本的寬高paint.getTextBounds(GooViewText, 0, GooViewText.length(), rect);int textWidth = rect.width();int textHeight = rect.height();//注意:一般控件是以左上角為基準點,文本是以左下角為基準點的,故:x為拖拽圓圓心x坐標-文本寬度/2float x=dragCenter.x-textWidth*0.5f;// y為拖拽圓圓心y坐標+文本寬度/2float y=dragCenter.y+textHeight*0.5f;canvas.drawText(GooViewText,x, y, paint);paint.setColor(Color.RED); }添加為GooView初始化位置的方法
public GooView initGooViewPosition(float rawX, float rawY) {stableCenter.set(rawX, rawY);dragCenter.set(rawX, rawY);return this; }處理適配器為GooView設置觸摸監聽
@Override protected void convert(BaseViewHolder helper, Msg item) {... //為控件設置觸摸監聽tv_un_read_msg_count.setOnTouchListener(listener); }觸摸監聽的實現:添加GooView并設置位置和文本
public class OnShowGooViewTouchListener implements View.OnTouchListener {private Context context;private GooView gooView;private WindowManager windowManager;private WindowManager.LayoutParams params;//處理構造方法,傳入上下文public OnShowGooViewTouchListener(Context context) {this.context = context;//創建GooViewgooView = new GooView(context);//創建WindowManager,可以用來在任何界面上添加一個額外的視圖windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);//為GooView設置布局參數params = new WindowManager.LayoutParams();//設置GooView的寬高為MATCH_PARENTparams.height = WindowManager.LayoutParams.MATCH_PARENT;params.width = WindowManager.LayoutParams.MATCH_PARENT;//設置GooView為透明,使得GooView出現后,用戶可以看到下面的界面params.format = PixelFormat.TRANSLUCENT;}private View mView;//重寫onTouch方法.//和onTouchEvent類似,如果onTouchEvent返回true,OnTouchListener返回true,則優先將事件交給MotionEvent@Overridepublic boolean onTouch(View v, MotionEvent event) {//當按下去的對TextView進行相關處理if (event.getAction() == MotionEvent.ACTION_DOWN) {msg = (Msg) v.getTag();String text = ((TextView) v).getText().toString();//隱藏TextViewv.setVisibility(View.INVISIBLE);mView = v;//獲取按下的x,y坐標float rawX = event.getRawX();float rawY = event.getRawY();//設置gooView顯示的位置和文本gooView.initGooViewPosition(rawX, rawY);gooView.setGooViewText(text);//把GooView加載到windowManager上顯示windowManager.addView(gooView, params);}//表示想要處理事件return true;} }出現的bug1:觸摸TextView然后向上移動,GooView不懂,RecyclerView動
事件被RecyclerView攔截了,需要請求RecyclerView不要攔截事件。事件分發涉及的基本概念:
- onTouchEvent:觸摸事件的處理
- dispatchTouchEvent:傳遞觸摸事件
- onInterceptTouchEvent:攔截事件傳遞
- requestDisallowedInterceptTouchEvent(boolean disallowIntercept):請求自己的父布局不要攔截事件
事件分發原理:解決觸摸監聽優于onTouchEvent獲取事件的問題,處理:
//請求RecyclerView不要搶奪事件 v.getParent():獲取TextView的父親布局,為條目的根部局 //requestDisallowInterceptTouchEvent():請求被調用者的父布局不要攔截事件,即RecyclerView不要攔截事件 v.getParent().requestDisallowInterceptTouchEvent(true);出現的bug2:移動后抬起手,GooView不消失
原因:利用WindowManger添加的視圖比較特殊,不能直接移除,需要用windowManager.removeView(view);來移除
但,作為OnShowGooViewTouchListener并不知道什么時候該移除GooView,故:使用接口回調來處理這個問題
在GooView中定義接口:
//定義接口 public interface OnGooViewChangedListener {//消失的回調public void disappear();//重置的回調public void reset(); } private OnGooViewChangedListener onGooViewChangedListener; public void setOnGooViewChangedListener( OnGooViewChangedListener onGooViewChangedListener) {this.onGooViewChangedListener = onGooViewChangedListener; }當消失的時候調用消失的回調方法:
case MotionEvent.ACTION_UP:...if (isOutOfRange) {if (distance > maxDistance) {...//調用接口消失的方法if (onGooViewChangedListener != null) {onGooViewChangedListener.disappear();}} else {...//調用接口重置的方法if (onGooViewChangedListener != null) {onGooViewChangedListener.reset();}}} else {...valueAnimator.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationEnd(Animator animation) {//調用接口重置的方法if (onGooViewChangedListener != null) {onGooViewChangedListener.reset();}}});...}invalidate();break;在OnShowGooViewTouchListener中設置和實現監聽,并實現兩個回調方法
public class OnShowGooViewTouchListener implements View.OnTouchListener, GooView.OnGooViewChangedListener { public OnShowGooViewTouchListener(Context context) {...gooView.setOnGooViewChangedListener(this);}//當GooView消失的時候,從WindowManager中移除@Overridepublic void disappear() {if (gooView.getParent() != null) {if (msg != null) {msg.unReadMsgCount = 0;}windowManager.removeView(gooView);}}//當GooView重置的時候,將GooView從WindowManager中移除@Overridepublic void reset() {if (gooView.getParent() != null) {windowManager.removeView(gooView);}//顯示TextViewmView.setVisibility(View.VISIBLE);} }總結
以上是生活随笔為你收集整理的自定义控件:QQ气泡效果粘性控件的实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 自定义控件:侧拉删除
- 下一篇: 自定义控件:Viewpager