ViewGroup篇:玩一下自定義ViewGroup

張風捷特烈發表於2019-02-20

自定義ViewGroup可不像自定義View那麼簡單

今日聚焦:
1.自定義ViewGroup中花樣佈局子View
2.移動View用layout、translation、TranslationAnimation、ViewPropertyAnimator、scrollTo、scrollBy有什麼區別?
3.ViewGroup裡怎麼給孩子加動畫
4.慣性移動? 也許你可以瞭解一下
複製程式碼
效果 1 效果 2
ViewGroup篇:玩一下自定義ViewGroup
ViewGroup篇:玩一下自定義ViewGroup

一、前置知識:

1.生命函式

這是我曾經測試畫出的一張圖,描述了ViewGroup+兩個孩子的生命函式呼叫情況

在這補充一點,ViewGroup在沒有背景時不會走onDraw方法,但可以走dispatchDraw
原因在於View對onDraw的控制時做了限定:[if (!dirtyOpaque) onDraw(canvas)]  
你可以使用onDraw,在之前設個透明色即可:setBackgroundColor(0x00000000);
複製程式碼

ViewGroup篇:玩一下自定義ViewGroup


2.View與Activity之間

貌似一直沒有對Activity與View的生命週期一起做過測試
測試之後發現View載入完成之後(onFinishInflate)並未立即回撥測量、佈局、繪製
而是在onResume之後View才會回撥onAttachedToWindow-->onMeasure-->onSizeChanged-->onLayout-->onDraw
這一點確實讓我挺驚訝,以前竟然沒注意,現在理清了,通暢很多

2019-02-19 16:50:29.998 : onCreate --------------
2019-02-19 16:50:29.992 : 建構函式: 0
2019-02-19 16:50:29.996 : onFinishInflate: 0
2019-02-19 16:50:33.001 : onStart: ...................
2019-02-19 16:50:33.006 : onResume: ...................
2019-02-19 16:50:33.050 : onAttachedToWindow: 
2019-02-19 16:50:33.207 : onMeasure: 0
2019-02-19 16:50:33.243 : onMeasure: 0
2019-02-19 16:50:33.354 : onSizeChanged: 1948
2019-02-19 16:50:33.358 : onLayout: 1948
2019-02-19 16:50:33.395 : onDraw: 1948
複製程式碼

二、自定義ViewGroup (排兵佈陣)

經測試發現注意點:

[1].必須onMeasure中測量孩子的尺寸,否則無法顯示
[2].必須onLayout中佈局孩子的位置,否則無法顯示
[3].在onLayout中孩子不能用view.getHeight()獲取尺寸(因為為0),只能用view.getMeasuredHeight
複製程式碼
1.最簡形式的ViewGroup

這裡使用介面卡模式,跟ListView一個套路,其實是非常簡單,看箭頭所指
這裡暫時不對ViewGroup進行測量,先填滿。對子View用自帶的測量方法measureChildren

public class FlowerLayout extends ViewGroup {
    private int mRadius;
    private static final String TAG = "FlowerLayout";
--->private ListAdapter mAdapter;
--->public void setAdapter(ListAdapter adapter) {
        mAdapter = adapter;
    }
    public FlowerLayout(Context context) {
        this(context, null);
    }
    public FlowerLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public FlowerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
        setBackgroundColor(0x55D3E8FD);
    }
    private void init(AttributeSet attrs) {
    }
    private void formFlower() {
        for (int i = 0; i < mAdapter.getCount(); i++) {
    --->    View petal = mAdapter.getView(i, null, this);
            addView(petal);//填入花瓣
        }
    }
    @Override
    protected void onAttachedToWindow() {
        Log.e(TAG, "onAttachedToWindow: ");
--->    if (mAdapter != null) {
            formFlower();
        }
        super.onAttachedToWindow();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //TODO 佈局子view
    }
}

---->[Activity中使用]----------------
setContentView(R.layout.activity_flower);
FlowerLayout flowerLayout = findViewById(R.id.id_fl);
ArrayList<Petal> petals = new ArrayList<>();
petals.add(new Petal(R.mipmap.icon_1, "icon_1"));
petals.add(new Petal(R.mipmap.icon_2, "icon_2"));
petals.add(new Petal(R.mipmap.icon_3, "icon_3"));
petals.add(new Petal(R.mipmap.icon_4, "icon_4"));
petals.add(new Petal(R.mipmap.icon_5, "icon_5"));
petals.add(new Petal(R.mipmap.icon_6, "icon_6"));
petals.add(new Petal(R.mipmap.icon_7, "icon_7"));
petals.add(new Petal(R.mipmap.icon_8, "icon_8"));
petals.add(new Petal(R.mipmap.icon_9, "icon_9"));
petals.add(new Petal(R.mipmap.icon_10, "icon_10"));
flowerLayout.setAdapter(new FlowerAdapter(petals));

---->[FlowerAdapter檢視介面卡]---------------------------
public class FlowerAdapter extends BaseAdapter {
    private List<Petal> mPetals;
    public FlowerAdapter(List<Petal> petals) {
        mPetals = petals;
    }
    @Override
    public int getCount() {
        return mPetals.size();
    }
    @Override
    public Object getItem(int position) {
        return mPetals.get(position);
    }
    @Override
    public long getItemId(int position) {
        return position;
    }
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        View view = inflater.inflate(R.layout.flower_item, parent, false);
        ImageView iv = view.findViewById(R.id.id_pic);
        iv.setImageResource(mPetals.get(position).resId);
        TextView tv = view.findViewById(R.id.id_info);
        tv.setText(mPetals.get(position).info);
        return view;
    }
}

複製程式碼

2.佈局子view

這裡關鍵在排布這裡給張圖先:子View佈局的左上右下

ViewGroup篇:玩一下自定義ViewGroup

---->[FlowerLayout#onLayout]----------------
 int childCount = getChildCount();
 for (int i = 0; i < childCount; i++) {
     View childView = getChildAt(i);
     int childW = childView.getMeasuredWidth();
     int childH = childView.getMeasuredHeight();
     int topPos = (int) (childH * i*0.5f);
     int leftPos = 0;
     childView.layout(leftPos, topPos, leftPos + childW, topPos + childH);
 }
 
 |-- 現在只要修改topPos和leftPos就可以改變子View佈局
複製程式碼

ViewGroup篇:玩一下自定義ViewGroup


3.放置成圓形

ViewGroup篇:玩一下自定義ViewGroup

圓形佈局.png

---->[FlowerLayout#onLayout]----------------
int count = mAdapter.getCount();
for (int i = 0; i < count; i++) {
    View childView = getChildAt(i);
    int childW = childView.getMeasuredWidth();
    int childH = childView.getMeasuredHeight();
    mRadius = (getWidth()-childW) / 2;
    float posX = childW / 2 + mRadius - mRadius * cos(i * 360.f / count);
    float posY = childH / 2 + mRadius - mRadius * sin(i * 360.f / count);
    int leftPos = (int) (posX - childW / 2);
    int topPos = (int) (posY - childH / 2);
    childView.layout(leftPos, topPos, leftPos + childW, topPos + childH);
}

private float cos(float θ) {
    return (float) Math.cos(θ / 180 * Math.PI);
}
private float sin(float θ) {
    return (float) Math.sin(θ / 180 * Math.PI);
}
複製程式碼

4.新增點選事件

這就比較容易了,一個監聽搞定

ViewGroup篇:玩一下自定義ViewGroup

//在形成View之後就新增點選事件
private void formFlower() {
   for (int i = 0; i < mAdapter.getCount(); i++) {
       View petal = mAdapter.getView(i, null, this);
       int position = i;
       if (mOnItemClickListener != null) {
           petal.setOnClickListener(v -> {
              ObjectAnimator.ofFloat(v, "ScaleX", 1f, 0.8f,1f).setDuration(200).start();
               ObjectAnimator.ofFloat(v, "ScaleY", 1f, 0.8f,1f).setDuration(200).start();
               mOnItemClickListener.onClick(v, this, position);
           });
       }
       addView(petal);//填入花瓣
   }


//----------------------------條目點選監聽-------------------
public interface OnItemClickListener {
    void onClick(View v, ViewGroup viewGroup, int position);
}
private OnItemClickListener mOnItemClickListener;
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
    mOnItemClickListener = onItemClickListener;
}

複製程式碼
5.陣列點陣定位

這突然讓我想到一個好玩的東西,那就是點陣控位。
點陣控位可以使用二維陣列,也可以使用字串,也可以使用畫素點。
具體的可以詳見我的這篇:這裡就放一張核心的分析圖:我們這裡不畫圓,而是取點位

ViewGroup篇:玩一下自定義ViewGroup

/**
 * 用來顯示點陣的二維陣列
 */
public static final int[][] digit_test = new int[][]
        {
                {0, 0, 0, 1, 0, 0, 0},
                {0, 0, 1, 0, 1, 0, 0},
                {0, 0, 1, 0, 1, 0, 0},
                {0, 1, 0, 1, 0, 1, 0},
                {0, 1, 0, 0, 0, 1, 0},
                {0, 1, 0, 0, 0, 1, 0},
                {0, 0, 0, 0, 0, 0, 0},
                {0, 0, 0, 0, 0, 0, 0},
        };
        
/**
 * 點位解析器
 * @param w 單體寬
 * @param h 單體高
 * @return 解析成的點位陣列
 */
private List<Point> renderDigit(int w, int h) {
    List<Point> points = new ArrayList<>();
    for (int i = 0; i < digit_test.length; i++) {
        for (int j = 0; j < digit_test[j].length; j++) {//一行一行遍歷,遇到1就畫
            if (digit_test[i][j] == 1) {
                int rX = (j * 2 + 1) * (w + 1);//第(i,j)個點圓心橫座標
                int rY = (i * 2 + 1) * (h + 1);//第(i,j)個點圓心縱座標
                points.add(new Point(rX, rY));
            }
        }
    }
    return points;
}

---->[onLayout使用點位]-------------------
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    List<Point> points = renderDigit(
            getChildAt(0).getMeasuredWidth() / 2,
            getChildAt(0).getMeasuredHeight() / 2
    );
    int count = mAdapter.getCount();
    for (int i = 0; i < count; i++) {
        View childView = getChildAt(i);
        int childW = childView.getMeasuredWidth();
        int childH = childView.getMeasuredHeight();
        mRadius = (getWidth() - childW) / 2;
        int leftPos = (int) (points.get(i).x - childW / 2);
        int topPos = (int) (points.get(i).y - childH / 2);
        childView.layout(leftPos, topPos, leftPos + childW, topPos + childH);
    }
}
複製程式碼

ViewGroup篇:玩一下自定義ViewGroup

ok了,只要把1放在你想要的位置,子View就在那裡,
不過簡單一點的還好說,要是愛心...來看神技:


5.點陣圖點陣定位

用黑白(就相當於上面1,0)來標識點位,再根據Bitmap的畫素進行
Bitmap記憶體殺手? 7*7畫素的Bitmap也就九牛一毛...
就是下面的小不點,你可以下載玩玩。有PS,你也可以用ps自己戳點


我在這裡呢....


畫素點位.png

mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.heart);

/**
 * 點位解析器
 *
 * @param bitmap bitmap
 * @param w 單體寬
 * @param h 單體高
 * @return 解析成的點位陣列
 */
public static List<Point> renderBitmap(Bitmap bitmap, int w, int h) {
    List<Point> points = new ArrayList<>();
    for (int i = 0; i < bitmap.getWidth(); i++) {
        for (int j = 0; j < bitmap.getHeight(); j++) {
            int pixel = bitmap.getPixel(i, j);
            if (pixel != -1) {//此處過濾掉白顏色
                int rX = (i * 2 + 1) * (w + 1);//第(i,j)個點圓心橫座標
                int rY = (j * 2 + 1) * (h + 1);//第(i,j)個點圓心縱座標
                points.add(new Point(rX, rY));
            }
        }
    }
    return points;
}
複製程式碼

到這裡排兵佈陣就結束了,相信對onLayout已經能玩的6了吧,接下來上陣殺敵。


二.移動測試篇

既然是測試,就一切從簡,直切問題本身,這裡新建了一個Activity
並且開啟了手機自帶的佈局便界顯示,這樣更能說明問題所在

1.佈局

自定義:TestViewGroup+TestView純原生,不加防腐劑
為了說明問題,這裡的TestViewGroup加了邊距20dp

佈局介面.png

<?xml version="1.0" encoding="utf-8"?>
<com.toly1994.analyzer.widget.TestViewGroup
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fl"
    android:layout_marginTop="20dp"
    android:layout_marginStart="20dp"
    android:layout_width="300dp"
    android:layout_height="200dp"
    android:background="#5597FFFA">
    <com.toly1994.analyzer.widget.TestView
        android:id="@+id/view"
        android:layout_width="200dp"
        android:layout_height="50dp"
        android:background="#23F627"/>
</com.toly1994.analyzer.widget.TestViewGroup>
複製程式碼

2.程式碼實現
/**
 * 作者:張風捷特烈<br/>
 * 時間:2019/2/20/020:10:30<br/>
 * 郵箱:1981462002@qq.com<br/>
 * 說明:測試ViewGroup
 */
public class TestViewGroup extends ViewGroup {
    public TestViewGroup(Context context) {
        super(context);
    }
    public TestViewGroup(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
    }
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        View child = getChildAt(0);
        child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
    }
}

/**
 * 作者:張風捷特烈<br/>
 * 時間:2019/2/20/020:10:30<br/>
 * 郵箱:1981462002@qq.com<br/>
 * 說明:測試View
 */
public class TestView extends View {
    public TestView(Context context) {
        super(context);
    }
    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
}
複製程式碼

3.移動:layout與translation

這樣對比應該非常明顯:layout真的把佈局移動了,translation只是離家出走而已

layout----- translation
layout.gif
translation.gif
點選事件在綠色上 點選事件在綠色上
---->[TestViewGroup#onTouchEvent]-------------
@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                int x = (int) event.getX();
                int y = (int) event.getY();
//                useLayout(mChild, x, y);
                useTranslation(mChild, x, y);
        }
        return super.onTouchEvent(event);
    }

    private void useLayout(View view, int x, int y) {
        view.layout(x, y,
                x + view.getMeasuredWidth(), y + view.getMeasuredHeight());
        //以下四行等價上一行
//                mChild.setLeft(x);
//                mChild.setTop(y);
//                mChild.setRight(x + mChild.getMeasuredWidth());
//                mChild.setBottom(y + mChild.getMeasuredHeight());
    }

    private void useTranslation(View view, int x, int y) {
        view.setTranslationX(x);
        view.setTranslationY(y);
    }
複製程式碼

4 : 移動動畫和屬性動畫

移動動畫都是從家裡開始,屬性動畫移動從當前位置,但是家還在那裡!
也就是傳說中的屬性動畫也並無法改變View的佈局位置

TranslationAnimation ViewPropertyAnimator
TranslateAnimation.gif
ViewPropertyAnimator.gif
點選事件在家裡 點選事件在綠色上
private void useTranslationAnimation(View view, int x, int y) {
    TranslateAnimation translateAnimation = new TranslateAnimation(0, x, 0, y);
    translateAnimation.setDuration(500);
    translateAnimation.setFillAfter(true);
    view.startAnimation(translateAnimation);
}

private void useViewPropertyAnimator(View view, int x, int y) {
    view.animate().translationX(x).translationY(y).setDuration(500).start();
    //下兩句效果同上
//       ObjectAnimator.ofFloat(view, "translationX", x).setDuration(500).start();
//       ObjectAnimator.ofFloat(view, "translationY", y).setDuration(500).start();
}
複製程式碼

5:哥就像讓View搬家動畫怎麼辦?

很簡單:ValueAnimator唄,在重新整理時對layout進行更新
由於有四個setXXX方法,這裡,簡單一點,使用ObjectAnimator

對Layout進行屬性動畫.gif

private void useLayoutAnimate(View view, int x, int y) {
    //下兩句效果同上
    ObjectAnimator.ofInt(view, "Left", x).setDuration(500).start();
    ObjectAnimator.ofInt(view, "Top", y).setDuration(500).start();
    ObjectAnimator.ofInt(view, "Right", x+view.getMeasuredWidth()).setDuration(500).start();
    ObjectAnimator.ofInt(view, "Bottom", y + view.getMeasuredHeight()).setDuration(500).start();
}
複製程式碼

6:scrollTo和scrollBy

可以說這兩個方法和上面的不是一輩的人,應用場景有很大區別
這兩個方法是移動一個View內部的所有子View,呼叫方並非子View
至於To和By的區別,也是老生常談,看圖體悟吧...

scrollTo scrollBy
ViewGroup篇:玩一下自定義ViewGroup
ViewGroup篇:玩一下自定義ViewGroup
---->[onTouchEvent]---------------
useScrollTo(-x, -y);
useScrollBy(-x, -y);

--------------------------------------------
private void useScrollTo(int x, int y) {
    scrollTo(x, y);
}
private void useScrollBy(int x, int y) {
    scrollBy(x, y);
}
複製程式碼

Ok ,基礎知識就到這裡,言歸正傳:


三、新增動畫

下面這幅圖應該不難吧,如果做不出來...下面的就當看風景吧...

靜態 動態
ViewGroup篇:玩一下自定義ViewGroup
ViewGroup篇:玩一下自定義ViewGroup

1.首先把排成圓的方法封裝一下
/**
 * @param start 第一個排成圓的View索引
 * @param dθ    旋轉角度
 */
private void layoutCircle(int start, float dθ) {
    int count = getChildCount();
    for (int i = start; i < count; i++) {
        View childView = getChildAt(i);
        int childW = childView.getMeasuredWidth();
        int childH = childView.getMeasuredHeight();
        int r = (getWidth() - childW) / 2;
        float posX = childW / 2 + r - r * cos(i * 360.f / (count - 1) + dθ);
        float posY = childH / 2 + r - r * sin(i * 360.f / (count - 1) + dθ);
        int leftPos = (int) (posX - childW / 2);
        int topPos = (int) (posY - childH / 2);
        childView.layout(leftPos, topPos, leftPos + childW, topPos + childH);
    }
}
複製程式碼

2.ValueAnimator走起

在點選的時候觸發mAnimator.start()即可

mAnimator = ValueAnimator.ofInt(0, 360);
mAnimator.setDuration(3000);
mAnimator.addUpdateListener(a -> {
    int deg = (int) a.getAnimatedValue();
    layoutCircle(1, deg);
});
複製程式碼

3.位置交換的功能

這裡實現和中心的交換,並且加入移動動畫

無動畫 有動畫
ViewGroup篇:玩一下自定義ViewGroup
ViewGroup篇:玩一下自定義ViewGroup
---->[維護成員變數]-------------
private int centerId = 0;//預設中心點

/**
 * 交換兩個View的位置
 * @param positionMe 點選者
 * @param positionHe 目標
 */
private void swap(int positionMe, int positionHe) {
    View me = getChildAt(positionMe);
    View he = getChildAt(positionHe);
    int TempMeLeft = me.getLeft();
    int TempMeTop = me.getTop();
    int TempMeRight = me.getRight();
    int TempMeBottom = me.getBottom();
    me.layout(he.getLeft(), he.getTop(), he.getRight(), he.getBottom());
    he.layout(TempMeLeft, TempMeTop, TempMeRight, TempMeBottom);
    centerId = positionMe;
}

|--然後只需要在需要的時候觸發即可:
swap(position, centerId);

複製程式碼

動畫,剛才貌似寫過了,直接拿來用

/**
 * 交換兩個View的位置
 * @param positionMe 點選者
 * @param positionHe 目標 
 */
private void swapWithAnim(int positionMe, int positionHe) {
    View me = getChildAt(positionMe);
    View he = getChildAt(positionHe);
    int TempMeLeft = me.getLeft();
    int TempMeTop = me.getTop();
    useLayoutAnimate(me, he.getLeft(), he.getTop());
    useLayoutAnimate(he, TempMeLeft,TempMeTop);
    centerId = positionMe;
}
private void useLayoutAnimate(View view, int x, int y) {
    ObjectAnimator.ofInt(view, "Left", x).setDuration(500).start();
    ObjectAnimator.ofInt(view, "Top", y).setDuration(500).start();
    ObjectAnimator.ofInt(view, "Right", x + view.getMeasuredWidth()).setDuration(500).start();
    ObjectAnimator.ofInt(view, "Bottom", y + view.getMeasuredHeight()).setDuration(500).start();
}
複製程式碼

既然可以動畫,那麼則麼玩都可以,比如旋轉和放大
動畫就不展開了,詳情可見:Android 動畫 Animator 家族使用指南

旋轉 放大
ViewGroup篇:玩一下自定義ViewGroup
ViewGroup篇:玩一下自定義ViewGroup

三、你覺得無聊,玩點6的

1.神技之一:VelocityTracker

這個類估計聽過的人不多,翻譯出來是速度追蹤器,作為一個好用的類,在此拎出來講一講
它的作用是獲取你滑動的x,y的速度x 左負,y上負

---->[FlowerLayout#init]---------------
private void init(AttributeSet attrs) {
    ...
    velocityTracker = VelocityTracker.obtain();//1.VelocityTracker的建立
}

---->[FlowerLayout#onTouchEvent]---------------
@Override
public boolean onTouchEvent(MotionEvent event) {
    View centerView = getChildAt(0);
    velocityTracker.addMovement(event);//2.VelocityTracker與event結合
    switch (event.getAction()) {
          case MotionEvent.ACTION_DOWN:
              ...
              break;
        case MotionEvent.ACTION_MOVE:
            velocityTracker.computeCurrentVelocity(1000);//3.計算速度
            //4.獲取值
            Log.e(TAG, "X velocity: " + velocityTracker.getXVelocity()+
                    "--Y velocity: " + velocityTracker.getYVelocity());
            break;
        case MotionEvent.ACTION_UP:
              ...
            break;
    }
    return true;
}
|--注意第5點:在適當的地方取消和回收
velocityTracker.clear();//取消
velocityTracker.recycle();//回收

|---我們比較在意的是計算速度的方法,1000是搞嘛的?
/**
 * Equivalent to invoking {@link #computeCurrentVelocity(int, float)} with a maximum
 * velocity of Float.MAX_VALUE.
 * 也就是說這裡的第三參是Float的最大值,表示這個速度足以超光速
 * @see #computeCurrentVelocity(int, float) 
 */
public void computeCurrentVelocity(int units) {
    nativeComputeCurrentVelocity(mPtr, units, Float.MAX_VALUE);
}

 /**
  * @param units The units you would like the velocity in.  A value of 1
  * provides pixels per millisecond, 1000 provides pixels per second, etc.
    你想要的單位是速度。值1表示畫素/毫秒,1000表示畫素/秒,等等。
  * @param maxVelocity The maximum velocity that can be computed by this method.
  * This value must be declared in the same unit as the units parameter. This value
  * must be positive. 該方法可以計算的最大值
  */
 public void computeCurrentVelocity(int units, float maxVelocity) {
     nativeComputeCurrentVelocity(mPtr, units, maxVelocity);
 }
 |-- native方法就不挖了
複製程式碼

2.有了速度能幹嘛?

慣性.gif

接下來的這部分源於陳小緣Android實現圓弧滑動效果之ArcSlidingHelper篇
我認真研究了一下,並融入了本ViewGroup,他封裝的非常好,我拆了一下擷取了和慣性相關的部分
不懂的可以去深度一下,我就不賣弄脣舌了,GitHub在:ArcSlidingHelper

---->[FlowerLayout#onLayout]--------------------
private void initRotate() {
    int width = getWidth();
    int height = getHeight();
    mPivotX = width/2;
    mPivotY = height/2;
    mVelocityTracker = VelocityTracker.obtain();
    mScrollAvailabilityRatio = .3F;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    View centerView = getChildAt(0);
    float x, y;
    x = event.getRawX();
    y = event.getRawY();
    mVelocityTracker.addMovement(event);
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mAnimator.start();
            abortAnimation();
              centerView.layout(x, y,
                      x + centerView.getMeasuredWidth(), y + centerView.getMeasuredHeight());
            Log.e("EVENT", "onTouchEvent: " + x + "------" + y);
            break;
        case MotionEvent.ACTION_MOVE:
            handleActionMove(x, y);
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_OUTSIDE:
            mVelocityTracker.computeCurrentVelocity(1000);
            mScroller.fling(0, 0,
                    (int) mVelocityTracker.getXVelocity(),
                    (int) mVelocityTracker.getYVelocity(),
                    Integer.MIN_VALUE, Integer.MAX_VALUE,
                    Integer.MIN_VALUE, Integer.MAX_VALUE);
            startFling();
            break;
    }
    mStartX = x;
    mStartY = y;
    return true;
}


//------------------------慣性旋轉----------------------------
private Scroller mScroller = new Scroller(getContext());
private int mPivotX, mPivotY;
private float mStartX, mStartY;
private float mLastScrollOffset;
private float mScrollAvailabilityRatio;
private boolean isClockwiseScrolling;
private boolean isShouldBeGetY;
private boolean isRecycled;
private VelocityTracker mVelocityTracker;
private Handler mHandler = new Handler(msg -> {
    computeInertialSliding();
    return false;
});
/**
 * 處理慣性滾動
 */
private void computeInertialSliding() {
    checkIsRecycled();
    if (mScroller.computeScrollOffset()) {
        float y = ((isShouldBeGetY ? mScroller.getCurrY() : mScroller.getCurrX()) * mScrollAvailabilityRatio);
        if (mLastScrollOffset != 0) {
            float offset = fixAngle(Math.abs(y - mLastScrollOffset));
            float deg = isClockwiseScrolling ? offset : -offset;
            setRotation(getRotation() + deg);
        }
        mLastScrollOffset = y;
        startFling();
    } else if (mScroller.isFinished()) {
        mLastScrollOffset = 0;
    }
}
/**
 * 計算滑動的角度
 */
private void handleActionMove(float x, float y) {
    float l, t, r, b;
    if (mStartX > x) {
        r = mStartX;
        l = x;
    } else {
        r = x;
        l = mStartX;
    }
    if (mStartY > y) {
        b = mStartY;
        t = y;
    } else {
        b = y;
        t = mStartY;
    }
    float pA1 = Math.abs(mStartX - mPivotX);
    float pA2 = Math.abs(mStartY - mPivotY);
    float pB1 = Math.abs(x - mPivotX);
    float pB2 = Math.abs(y - mPivotY);
    float hypotenuse = (float) Math.sqrt(Math.pow(r - l, 2) + Math.pow(b - t, 2));
    float lineA = (float) Math.sqrt(Math.pow(pA1, 2) + Math.pow(pA2, 2));
    float lineB = (float) Math.sqrt(Math.pow(pB1, 2) + Math.pow(pB2, 2));
    if (hypotenuse > 0 && lineA > 0 && lineB > 0) {
        float angle = fixAngle((float) Math.toDegrees(Math.acos((Math.pow(lineA, 2) + Math.pow(lineB, 2) - Math.pow(hypotenuse, 2)) / (2 * lineA * lineB))));
        float deg = (isClockwiseScrolling = isClockwise(x, y)) ? angle : -angle;
        setRotation(getRotation() + deg);
    }
}
/**
 * 打斷動畫
 */
public void abortAnimation() {
    checkIsRecycled();
    if (!mScroller.isFinished()) {
        mScroller.abortAnimation();
    }
}
/**
 * 釋放資源
 */
public void release() {
    checkIsRecycled();
    mScroller = null;
    mVelocityTracker.recycle();
    mVelocityTracker = null;
    isRecycled = true;
}
/**
 * 檢測手指是否順時針滑動
 *
 * @param x 當前手指的x座標
 * @param y 當前手指的y座標
 * @return 是否順時針
 */
private boolean isClockwise(float x, float y) {
    return (isShouldBeGetY = Math.abs(y - mStartY) > Math.abs(x - mStartX)) ?
            x < mPivotX != y > mStartY : y < mPivotY == x > mStartX;
}
/**
 * 開始慣性滾動
 */
private void startFling() {
    mHandler.sendEmptyMessage(0);
}
/**
 * 調整角度,使其在360之間
 *
 * @param rotation 當前角度
 * @return 調整後的角度
 */
private float fixAngle(float rotation) {
    float angle = 360F;
    if (rotation < 0) {
        rotation += angle;
    }
    if (rotation > angle) {
        rotation = rotation % angle;
    }
    return rotation;
}
/**
 * 檢查資源釋放已經釋放
 */
private void checkIsRecycled() {
    if (isRecycled) {
        throw new IllegalStateException(" is recycled!");
    }
}
複製程式碼

OK,今天就到這裡


後記:捷文規範

1.本文成長記錄及勘誤表
專案原始碼 日期 附錄
V0.1--github 2018-2-20

釋出名:View篇:玩一下自定義ViewGroup
捷文連結:juejin.im/post/5c6d19…

2.更多關於我
筆名 QQ 微信
張風捷特烈 1981462002 zdl1994328

我的github:github.com/toly1994328
我的簡書:www.jianshu.com/u/e4e52c116…
我的掘金:juejin.im/user/5b42c0…
個人網站:www.toly1994.com

3.宣告

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援

icon_wx_200.png

相關文章