1. 程式人生 > >自定義控制元件:QQ氣泡效果粘性控制元件的實現

自定義控制元件:QQ氣泡效果粘性控制元件的實現

學習目的

  • 瞭解幾何圖形工具的用法
  • 掌握畫不規則圖形的方法

應用場景:未讀提醒,效果圖:

繪製一幀的效果

畫一幀粘性控制元件的步驟分析

  1. 畫一個固定圓
  2. 畫一個拖拽圓
  3. 畫中間連線部分

將中間連線部分水平放置,四個角的座標定為固定值,分別標記上點的編號,矩形中心的點為控制元件點,畫曲線時用

自定義一個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); } @Override
protected 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 中

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        super.onCreate(savedInstanceState);
        setContentView(new GooView(this));
    }
}

貝塞爾曲線

上述程式碼呼叫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 的成員變數

// 固定圓圓心
PointF mStickCenter = new PointF(150f, 150f);
// 固定圓半徑
float mStickRadius = 12f;
// 拖拽圓圓心
PointF mDragCenter = new PointF(90f, 90f);
// 拖拽圓半徑
float mDragRadius = 16f;
// 固定圓的兩個附著點
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);
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    // 畫中間連線部分
    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 的正弦值為兩圓心縱座標之差比上橫座標之差,則角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 分母不能為0
        lineK = (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 分母不能為0
        lineK = (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 等於80f
    distance = 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

事件處理

事件處理的分析

  1. 超出最大範圍:拖拽圓與固定圓斷開,鬆手後消失
  2. 超出最大範圍:又放回去,恢復
  3. 沒有超出最大範圍:鬆手,回彈動畫,恢復

事件處理的實現

修改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() {

                    @Override
                    public 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() {

                    @Override
                    public 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() {
                    @Override
                    public 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 {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        super.onCreate(savedInstanceState);
        GooView view = new GooView(this);
        setContentView(view);
        view.setOnUpdateListener(new OnUpdateListener() {
            @Override
            public void onReset(boolean isOutOfRange) {
                Utils.showToast(getApplicationContext(), "onReset:"+isOutOfRange);
            }

            @Override
            public 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;
   }

   @Override
   public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
      View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.rlv_item, parent,false);
      return new MyViewHolder(view);
   }

   @Override
   public 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+"");
      }
   }

   @Override
   public 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座標-文字寬度/2
    float x=dragCenter.x-textWidth*0.5f;
    // y為拖拽圓圓心y座標+文字寬度/2
    float 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;
      //建立GooView
      gooView = new GooView(context);
  //建立WindowManager,可以用來在任何介面上新增一個額外的檢視
      windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
  //為GooView設定佈局引數
      params = new WindowManager.LayoutParams();
      //設定GooView的寬高為MATCH_PARENT
      params.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
   @Override
   public boolean onTouch(View v, MotionEvent event) {
    //當按下去的對TextView進行相關處理
      if (event.getAction() == MotionEvent.ACTION_DOWN) {
         msg = (Msg) v.getTag();
         String text = ((TextView) v).getText().toString();
         //隱藏TextView
         v.setVisibility(View.INVISIBLE);
         mView = v;
         //獲取按下的x,y座標
         float rawX = event.getRawX();
         float rawY = event.getRawY();
         //設定gooView顯示的位置和文字
         gooView.initGooViewPosition(rawX, rawY);
     gooView.setGooViewText(text);