源碼已同步到github:https://github.com/ykbjson/FocusView.git
閒話
本來最近在做一個IPTV的廣告插件包,因爲這個項目沒多少功能,僅僅涉及懸浮Window的相關操作,所以後面就很閒,看到別的同事在分析阿狸TV市場的一個炫酷效果,所以就索性自己嘗試仿效一下。
模仿效果
這個炫酷的效果大致如下:1.當視圖容器裏的子視圖發生焦點切換時,一個懸浮的焦點框(不是系統的)會從失去焦點的子視圖上飄移到獲得焦點的子視圖上,並且焦點框的大小逐漸變化爲獲得焦點視圖的大小;2,飄移完成後,獲得焦點的子視圖放大、焦點框放大,失去焦點的子視圖縮小。可能我描述起來大家覺得很抽象,有興趣的童鞋可以去到自家電視上找找(估計沒多少機頂盒有阿狸市場),或者運行我的demo,看看基礎效果。
過程分析
這裏先說明,由於我不是專門負責這個效果的,所以我實現第一個效果後就沒有往下繼續了,後續效果由別的同事研究,所以以下內容僅圍繞第一種效果展開。
估計大部分童鞋都會想到,要實現這個效果,使用的東西要麼是動畫,要麼是矩陣。不錯,我是用矩陣也實現了,但是矩陣出現的問題是.9的邊框放大後直接畫出來也會失真,所以我採用動畫。但是可能大部分童鞋都和我一樣只是常使用View Animation,我本來也想使用View Animation,但是發現控制焦點框寬高均勻動態變化好難(網上有教使用縮放&平移動畫混合並計算動畫偏移的方法可以實現這個效果,不過我智商太低,真的看不懂O(∩_∩)O),主要問題在於AnimationListener裏沒有動畫實時更新回調的相關接口,所以我只好另尋出路,於是乎,我認識了Property Animation。關於Property Animation,有興趣的童鞋請移步:PropertyAnimation詳解。
實施過程
1.一個自定義ViewGroup
這個容器的作用主要作用是控制焦點框的初始位置和大小以及對子視圖焦點變化事件的捕獲。
/**
* com.ykbjson.view.MFocusView
*
* @author Kebin.Yan
* @Description 子view獲得焦點時焦點框有動畫效果的視圖容器
* @date Create At :2015年7月2日 上午9:01:37
*/
public class MFocusView extends RelativeLayout implements OnFocusChangeListener, OnClickListener {
private final String TAG = getClass().getSimpleName();
/**
* 焦點框
*/
private FloatView floatView;
/**
* 是否初始化過
*/
private boolean isInit;
public MFocusView(Context context) {
this(context, null);
}
public MFocusView(Context context, AttributeSet attr) {
this(context, attr, 0);
}
public MFocusView(Context context, AttributeSet attr, int style) {
super(context, attr, style);
}
/**
* 初始化焦點框
*/
private void initFloatView()
{
int firstChildwidth = -1;
int firstChildHeight = -1;
//這個地方或許是不對的,因爲不一定是第一個視圖有焦點
View child = findFocus();
if(null==child)
getChildAt(0);
if (null != child) {
firstChildwidth = (int) (child.getRight() - child.getLeft());
firstChildHeight = (int) (child.getBottom() - child.getTop());
}
floatView = new FloatView(getContext());
LayoutParams floatParams = new LayoutParams(firstChildwidth, firstChildHeight);
floatView.setId(4);
floatView.setLayoutParams(floatParams);
floatView.setFocusable(false);
floatView.setFocusableInTouchMode(false);
floatView.setScaleType(ScaleType.FIT_XY);
floatView.setImageResource(R.drawable.focus_bound);
addView(floatView);
if (null != child)
floatView.onReLayout(child.getLeft(), child.getTop(), firstChildwidth, firstChildHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (changed && !isInit) {
isInit = !isInit;
if (getChildCount() > 0) {
Log.e(TAG, "onLayout,childCount : " + getChildCount());
for (int i = 0; i < getChildCount(); i++) {
//加這個是爲了在手機上測試
getChildAt(i).setOnClickListener(this);
//捕獲子視圖焦點
getChildAt(i).setOnFocusChangeListener(this);
}
}
initFloatView();
}
}
@Override
public void onClick(View child) {
onChildChange(child);
}
@Override
public void onFocusChange(View child, boolean hasFocus) {
Log.e(TAG, "onFocusChanged,hasFocus : " + hasFocus);
if (!hasFocus)
return;
onChildChange(child);
}
/**
* 焦點框視圖改變
*/
private void onChildChange() {
View child = findFocus();
onChildChange(child);
}
/**
* 焦點框視圖改變
*
* @param child 當前右焦有的視圖
*/
private void onChildChange(View child) {
if (null == child)
return;
float focusX = child.getLeft();
float focusY = child.getTop();
float focusW = child.getRight() - focusX;
float focusH = child.getBottom() - focusY;
floatView.onReLayout(focusX, focusY, focusW, focusH);
Log.e(TAG, "onChildChange,child : " + child);
}
}
這個類很簡單,就是默認裝載焦點框視圖到最頂層,捕獲到焦點變化後通知焦點框重繪和播放動畫。
2.一個ImageView
/**
* com.ykbjson.view.FloatView
*
* @author Kebin.Yan
* @Description 焦點框視圖
* @date Create At :2015年7月2日 上午9:02:14
*/
public class FloatView extends ImageView {
private final String TAG = getClass().getSimpleName();
/**
* x軸縮放率
*/
private float scalX = 1.0f;
/**
* y軸縮放率
*/
private float scalY = 1.0f;
/**
* 上一次view的x座標
*/
public float nFocusX;
/**
* 上一次view的y座標
*/
public float nFocusY;
/**
* 上一次view的寬度
*/
public int nFocusW;
/**
* 上一次view的高度
*/
public int nFocusH;
public FloatView(Context context) {
super(context);
}
/**
* 改變當前視圖位置和大小
*
* @param focusX 新的x座標
* @param focusY 新的y座標
* @param focusW 新的寬度
* @param focusH 新的高度
*/
public void onReLayout(final float focusX, final float focusY, final float focusW, final float focusH) {
scalX = focusW / getWidth();
scalY = focusH / getHeight();
ValueAnimator valueAnimator = new ValueAnimator();
valueAnimator.setObjectValues(new LayoutParams(nFocusW, nFocusH));
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.setEvaluator(new TypeEvaluator<LayoutParams>() {
@Override
public LayoutParams evaluate(float fraction, LayoutParams startValue, LayoutParams endValue) {
Log.e(TAG, "evaluate , fraction = " + fraction);
LayoutParams params = new LayoutParams(0, 0);
params.width = (int) ((focusW - getWidth()) * fraction);
params.height = (int) ((focusH - getHeight()) * fraction);
return params;
}
});
valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
LayoutParams nParams = (LayoutParams) animation.getAnimatedValue();
LayoutParams params = (LayoutParams) getLayoutParams();
params.width += nParams.width;
params.height += nParams.height;
setLayoutParams(params);
}
});
valueAnimator.setStartDelay(250);
AnimatorSet set = new AnimatorSet();
set.playTogether(ObjectAnimator.ofFloat(this, "translationX", nFocusX, focusX), ObjectAnimator.ofFloat(this, "translationY", nFocusY, focusY), valueAnimator);
set.setDuration(500);
set.setTarget(this);
set.start();
nFocusX = focusX;
nFocusY = focusY;
nFocusW = (int) focusW;
nFocusH = (int) focusH;
}
@Override
protected void onDraw(Canvas canvas) {
Log.e(TAG, "onDraw " + " scalX : " + scalX + " scalY : " + scalY + " nFocusX : " + nFocusX + " nFocusY : " + nFocusY);
super.onDraw(canvas);
}
}
這個ImageView主要是實現焦點框(它本身)的平移和縮放,需要看的地方就是AnimatorUpdateListener那裏,我就不再贅述了。
結語
存在的bug:
1.在多點觸控手機上同時按兩個按鈕時動畫混亂。
2.如果子視圖不自定義selector,點擊時系統原生的焦點框會先於這個飄移框出現
其實每次寫文章不是爲了貼代碼,是希望弄清楚一些原理,更希望提升自己的語言組織能力和表達能力。
我相信,肯定有數不盡的大牛有很多的實現方式,但我依然要寫出來,因爲我希望讓更多還未成爲大牛的人得到一個思路。在浩瀚的Android攻城獅隊伍中,我不過如那銀河裏的一粒星辰,即使渺小,但依然努力發光。