自定義View之案列篇(二):扇形菜單
今天帶給大家的是案例的第二篇(扇形菜單),先來啾啾它的容貌
sec
手指不斷的點擊,動畫來回收縮:
sec
對上面這個效果,大家一點不會陌生。下面一起來看看它的具體實現。
一、SectorLayout(扇形菜單)
我們首先來分析分析效果圖,展開時的動畫:
-
縮放 0 到 1
-
透明度 0 到 1
-
旋轉 0° 到 360°
-
半徑不斷增長,某角度的平移
收起菜單的動畫跟展開的動畫恰恰相反。
先來看看平移的動畫,看看下面的分析圖:
sec
我們這里利用三角函數的知識,很容易得到 B 點的坐標,那么 a 的角度為 90° / (childCount - 1) ,唯一變化的是 r 的值,那么它的區間取值為 [0,getHeight() - getChildAt(0).getWidth()] 。
縮放,旋轉,透明度的動畫處理比較簡單,這里就不再細講。文章的結尾處我會附上源碼下載地址。
1、onMeasure()
ViewGroup 的繪制流程 onMeasure() -> onLayout() -> onDraw() 。由于我們不會對視圖做任何繪制,所以 onDraw 方法這里就不做討論。先來理理 onMeasure 測量規格(計算尺寸)的方法, ViewGroup 測量規格包含了【測量自身】與【測量子視圖】。
【測量自身】:
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//處理 wrap_content問題
int defaultDimension = dip2px(200);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(defaultDimension, defaultDimension);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(defaultDimension, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, defaultDimension);
}
如果不處理 MeasureSpec.AT_MOST 模式下的情況,默認占據剩余空間的大小,那么當你設置屬性 wrap_content 的效果跟 match_parent 的效果是一樣的。如果包含 wrap_content 我們讓它占據 200dp 的一個大小,當然這個值可以根據你的需求而定。動態設置也是可以的。
注意了,本篇是沒有處理 padding 屬性的,如果你感興趣可以嘗試下。
【測量子視圖】:
private void measureChildViews() {
final int childCount = getChildCount();
final int childMode = MeasureSpec.EXACTLY;
final int childSize = Math.min(getMeasuredWidth(), getMeasuredHeight()) / 6;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (childView.getVisibility() == GONE) {
continue;
}
int measureSpec = -1;
measureSpec = MeasureSpec.makeMeasureSpec(childSize, childMode);
childView.measure(measureSpec, measureSpec);
}
}
子視圖的測量模式為 MeasureSpec.EXACTLY 確定模式,如果你在布局文件 xml 中新增子視圖,那么你可能需要改寫這里的測量模式;子視圖的測量大小為 【寬高的最小值 / 常數】,這里的常數你可以根據你的需求修改。 測量模式與測量大小決定測試規格
MeasureSpec.makeMeasureSpec(childSize, childMode);
childView.measure(measureSpec, measureSpec);
完成子視圖的一個測量。
onLayout()
ViewGroup 的 onLayout 同樣也是包含兩個部分 【計算自身位置】 與 【計算子視圖位置】。【計算自身位置】是根據你當前的布局文件而定,所以這里也不對其進行處理。主要分析【計算子視圖位置】的方法如下:
childView.layout(int l, int t, int r, int b)
你可以這樣來理解 4 個參數的含義: 左上角與右下角對角線兩點的坐標。
sec
根據分析圖就可以很容易的得到 childView.layout 的各個參數:
childView.layout(
//w-r*sina-(A,B小球的半徑之和)
width - (int) (mRadius * Math.sin(Math.toRadians(angle * i))) - childSize,
height - (int) (mRadius * Math.cos(Math.toRadians(angle * i))) - childSize,
width - (int) (mRadius * Math.sin(Math.toRadians(angle * i))),
height - (int) (mRadius * Math.cos(Math.toRadians(angle * i))));
處理旋轉,縮放已經透明度:
childView.setAlpha(mAlpha);
childView.setScaleX(mScale);
childView.setScaleY(mScale);
childView.setRotation(mRotation);
講到這里繪制流程差不多就完了,接著來看看動畫的設計。
動畫設計
相信大家已經看出來了,這里使用的是屬性動畫 property animation ,上文已經分析了主要是【展開動畫】,【收起動畫】。
它們都比較簡單:
【展開動畫】:
private ValueAnimator initAnimator(final boolean openAnimatorEnable) {
ValueAnimator animator = null;
if (getChildAt(0) == null) {
return animator;
}
animator = ValueAnimator.ofFloat(0f, 1.0f);
animator.setInterpolator(new BounceInterpolator());
animator.setDuration(2000);
animator.addUpdateListener(new MyAnimatorUpdateListener(this) {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mRadius = (int) ((float) valueAnimator.getAnimatedValue() * (getHeight() - getChildAt(0).getWidth()));
mAlpha = (float) valueAnimator.getAnimatedValue();
mScale = (float) valueAnimator.getAnimatedValue();
mRotation = (float) valueAnimator.getAnimatedValue() * ANGLE_360;
requestLayout();
}
});
return animator;
}
當我這里處理動畫,連續點擊菜單的展開和收起比較突兀,比較不理想。我想實現的效果是:當動畫正在展開并有沒有達到最高點,我再次點擊,動畫會以當前點收起動畫;當動畫正在收起并沒有達到最低點,我再次點擊,動畫會以當前點展開動畫。做到一個連貫的效果,我是這樣處理的:
if (openAnimatorEnable) {
mStartValue = 0f;
mEndValue = (float) valueAnimator.getAnimatedValue();
} else {
mEndValue = 1.0f;
mStartValue = (float) valueAnimator.getAnimatedValue();
}
來保存它的一個當前值。最后來刷新從繪 requestLayout(); 。
為了能夠更好的擴展,我這里加入了適配的方式來新增子視圖。
private void buildItems() {
for (int i = 0; i < mAdapter.getCount(); i++) {
final View itemView = mAdapter.getView(i, null, this);
addView(itemView);
}
}
二、使用方式
這里只需要注意一點:
@Override
public View getView(final int position, View convertView, final ViewGroup parent) {
holder holder = null;
if (convertView == null) {
holder = new holder();
convertView = View.inflate(MainActivity.this, R.layout.activity_item, null);
holder.mImage = (ImageView) convertView.findViewById(R.id.iv);
convertView.setTag(holder);
} else {
holder = (holder) convertView.getTag();
}
holder.mImage.setBackgroundResource(mImages[position]);
if (position != (getCount() - 1)) {
holder.mImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "" + position, Toast.LENGTH_SHORT).show();
}
});
}
return convertView;
}
if (position != (getCount() - 1)) 不要處理最后一個子視圖的點擊事件。因為最后一個子視圖固定在了右下角且點擊事件已經在 SectorLayout 中處理了。如果不加這句,你會發現點擊并沒有效果展示。
最后說一點,屬性動畫在監聽回調的方法當中可能持有外部類的一個引用,說白了就是持有 activity 的引用,導致 activity 不能被回收,造成內存泄漏。處理的方式是采用匿名的內部類結合弱引用處理。