如上圖效果腥刹,公司項目上個版本中需要這個效果拒垃,手機上這種效果幾乎是沒有的雀彼,答對了我們在開發(fā)機頂盒項目纺阔。下面就來說說這個效果的實現(xiàn)路程為了勾引你的欲望瘸彤,先上一下我實現(xiàn)的效果
Changed
- 最終思路段落:找到了GridLayout調(diào)用bringToFront()不錯亂的方法
- 具體實現(xiàn)段落:ZoomHoverAdapter的代碼更新
- 新增繼承GridLayout的實現(xiàn),可以設(shè)置跨行笛钝,跨列
實現(xiàn)思路歷程
第一印象:
開始看這個效果的時候质况,上來的第一印象就是用GridLayout,網(wǎng)格布局迅速搭好了然后測試下玻靡,點擊放大調(diào)用view的setScaleX()和setScaleY()方法拯杠,再在點擊的時候添加一個陰影背景哇!?信潭陪!這不搞定了嘛,運行一下看看效果最蕾。
啊叻叻~發(fā)現(xiàn)放大以后依溯,和其他子view重疊的區(qū)域被遮擋了...WHY?因為所有子view在同一層瘟则,所以就會遮擋黎炉!
項目版本中的實現(xiàn)
由于項目的版本比較緊,所以沒多長時間去研究醋拧,所以當時就采取了FrameLayout+GridLayout+帶陰影的layout 這個方案去實現(xiàn)的慷嗜。FrameLayout在最外層淀弹,GridLayout被包含在其中,還有一個帶有陰影的layout在GridLayout的上層沒點擊的時候GONE掉庆械;在點擊的時候獲取點擊的view然后copy一份到帶陰影l(fā)ayout中薇溃。效果是達到了,但是有缺點:
- 修改起來很麻煩缭乘,做不到動態(tài)添加
- 增加了布局的層級沐序,每次copy View效率也不高
當時沒辦法,只能先這樣實現(xiàn)后期優(yōu)化堕绩。
空白期思路探索
最近項目終于不忙了策幼,也有時間研究一些新東西,研究優(yōu)化的事情奴紧。準備優(yōu)化的時候特姐,首先想到了Android TV這種效果是自帶的,然后把Leanback的工程扒下來看了又看黍氮,最終鎖定了Presenter和ImageCardView上到逊,但是小弟不才,完全沒看懂內(nèi)部實現(xiàn)所以放棄了(**Q:**是不是可以用AndroidTV的api滤钱?**A:**抱歉,我們用的是廠家定制的api大多和手機api一樣脑题,版本為4.4)件缸,然后通過google發(fā)現(xiàn),一組神奇的類**ViewOverlay叔遂,ViewGroupOverlay(它是view的最上面的一個透明的層他炊,我們可以在這個層之上添加內(nèi)容而不會影響到整個布局結(jié)構(gòu)。這個層和我們的界面大小相同已艰,可以理解成一個浮動在界面表面的二維空間痊末。)**,嗯....ViewGroupOverlay很符合我的需求但是這個類有個奇怪的設(shè)定:向ViewGroupOverlay中添加view以后會把原來的view移除哩掺。,,,,,,,這不開玩笑嘛T涞!嚼吞!這個也不行~
最終思路
我最終的實現(xiàn)思路是通過View中的bringToFront()方法盒件。來看看官方解釋
/**
* Change the view's z order in the tree, so it's on top of other sibling
* views. This ordering change may affect layout, if the parent container
* uses an order-dependent layout scheme (e.g., LinearLayout). Prior
* to {@link android.os.Build.VERSION_CODES#KITKAT} this
* method should be followed by calls to {@link #requestLayout()} and
* {@link View#invalidate()} on the view's parent to force the parent to redraw
* with the new child ordering.
*
* @see ViewGroup#bringChildToFront(View)
*/
改變視圖z軸的次序,使它在兄弟姐妹視圖的頂部舱禽。
其實一開始群里大神和我提過這個方法炒刁,但是在GridLayout中使用會導(dǎo)致子view的位置錯亂,調(diào)用bringToFront()方法的view會移動到最后所以開始放棄了(這里更新:在GridLayout中如果給每個子view確定坐標則調(diào)用biringToFront后位置不會錯亂誊稚,感謝這篇回答)翔始。google真是很強大罗心,將問題復(fù)制進去搜到了一個結(jié)果,StackOverflow這個回答城瞎,里面答案是用RelativeLayout代替LinearLayout渤闷,看到這個回答后,我簡單試了下FrameLayout下調(diào)用發(fā)現(xiàn)也不會有錯亂全谤。所以在這兩個Layout中選擇一個7粝!认然!我毅然決然的選擇了RelativeLayout(因為還要實現(xiàn)網(wǎng)格布局那不是)补憾。
so~我最終通過繼承RelativeLayout,子view調(diào)用bringToFront()方法來實現(xiàn)>碓薄S摇!以上是我這一周的大腦的工作過程毕骡。
更新:由于找到了GridLayout位置不錯亂的實現(xiàn)方式而之前有了RelativeLayout的實現(xiàn)削饵,所以選用了兩種方式來實現(xiàn)了該效果
具體實現(xiàn)
接下來看具體代碼的實現(xiàn),我給它起名叫做ZoomHoverView(這個名字是我和一位美女一同起的我叫她越越(群里都叫她廢妹)未巫,感謝越越想到這么帥的名字~)窿撬,主要的類有三個:
- ZoomHoverAdapter:為ZoomHoverView提供子view(感謝鴻洋大神的FlowLayout項目給我啟發(fā))
- ZoomHoverView:繼承自RelativeLayout,setAdapter()后通過adapter去給子view添加布局規(guī)則叙凡,然后addView
- ZoomHoverGridView:繼承自GridLayout劈伴,新增實現(xiàn)方式,具體實現(xiàn)代碼見github
先來ZoomHoverAdapter實現(xiàn):
public abstract class ZoomHoverAdapter<T> {
private List<T> mDataList;
//數(shù)據(jù)變化回調(diào)
private OnDataChangedListener mOnDataChangedListener;
/**
* 數(shù)據(jù)變化回調(diào)
*/
interface OnDataChangedListener {
void onChanged();
}
/**
* 獲取數(shù)據(jù)的總數(shù)
*
* @return
*/
public int getCount() {
return mDataList == null ? 0 : mDataList.size();
}
/**
* 獲取對應(yīng)Item的bean
*
* @param position
* @return
*/
public T getItem(int position) {
if (mDataList == null) {
return null;
} else {
return mDataList.get(position);
}
}
public abstract View getView(ViewGroup parent, int position, T t);
以上為Adapter的所有代碼握爷,和我們常用的Adapter并無差別跛璧,另外,將原來的設(shè)置跨度這部分代碼移入了View中新啼。
接下來看ZoomHoverView實現(xiàn)
public class ZoomHoverView extends RelativeLayout implements View.OnClickListener, ZoomHoverAdapter.OnDataChangedListener {
//adapter
private ZoomHoverAdapter mZoomHoverAdapter;
// 需要的列數(shù)
private int mColumnNum = 3;
//記錄當前列
private int mCurrentColumn = 0;
//記錄當前行
private int mCurrentRow = 1;
//記錄每行第一列的下標(row First column position)
private SimpleArrayMap<Integer, Integer> mRFColPosMap= new SimpleArrayMap<>();
//子view距離父控件的外邊距寬度
private int mMarginParent = 20;
//行列的分割線寬度
private int mDivider = 10;
//當前放大動畫
private AnimatorSet mCurrentZoomInAnim = null;
//當前縮小動畫
private AnimatorSet mCurrentZoomOutAnim = null;
//縮放動畫監(jiān)聽器
private OnZoomAnimatorListener mOnZoomAnimatorListener = null;
//動畫持續(xù)時間
private int mAnimDuration;
//動畫縮放倍數(shù)
private float mAnimZoomTo;
//縮放動畫插值器
private Interpolator mZoomInInterpolator;
private Interpolator mZoomOutInterpolator;
//上一個ZoomOut的view(為了解決快速切換時追城,上一個被縮小的view縮放大小不正常的情況)
private View mPreZoomOutView;
//當前被選中的view
private View mCurrentView = null;
//item選中監(jiān)聽器
private OnItemSelectedListener mOnItemSelectedListener;
//存儲當前l(fā)ayout中所有子view
private List<View> mViewList;
所有成員變量的定義,都加了注釋燥撞,這里說一下一個大家有可能不明白的變量mRFColPosMap:RFColPos是Row First Column Position縮寫即每一行的第一列的下標座柱,由于我們是網(wǎng)格式布局再加上我們有的item需要拉伸,所以每一行的第一個位置是無法確定的物舒,所以用一個map存儲(K-所在行數(shù)辆布,V-view的下標)。還有一個mMarginParent屬性:因為我們點擊放大而子view放大以后無法超越父控件茶鉴,所以會造成邊上view放大被遮擋锋玲,需要設(shè)置mMarginParent,來控制與父邊框的距離涵叮。
/**
* 設(shè)置適配器
*
* @param adapter
*/
public void setAdapter(ZoomHoverAdapter adapter) {
this.mZoomHoverAdapter = adapter;
mZoomHoverAdapter.setDataChangedListener(this);
changeAdapter();
}
@Override
public void onChanged() {
changeAdapter();
}
這兩個方法沒什么說的惭蹂,接下來看看最關(guān)鍵的方法changeAdapter()
伞插,通過這個方法給每個子view設(shè)置布局規(guī)則,添加view:
/**
* 根據(jù)adapter添加view
*/
private void changeAdapter() {
removeAllViews();
//重置參數(shù)(因為changeAdapter可能調(diào)用多次)
mColumnNum = 3;
mCurrentRow = 1;
mCurrentColumn = 0;
mRFColPosMap.clear();
mViewList = new ArrayList<>(mZoomHoverAdapter.getCount());
//需要拉伸的下標的參數(shù)K-下標盾碗,V-跨度
SimpleArrayMap<Integer, Integer> needSpanMap = mZoomHoverAdapter.getSpanList();
for (int i = 0; i < mZoomHoverAdapter.getCount(); i++) {
//獲取子view
View childView = mZoomHoverAdapter.getView(this, i, mZoomHoverAdapter.getItem(i));
mViewList.add(childView);
childView.setId(i + 1);
//判斷當前view是否設(shè)置了跨度
int span = 1;
if (needSpanMap.containsKey(i)) {
span = needSpanMap.get(i);
}
//獲取AdapterView的的布局參數(shù)
RelativeLayout.LayoutParams childViewParams = (LayoutParams) childView.getLayoutParams();
if (childViewParams == null) {
childViewParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
//如果view的寬高設(shè)置了wrap_content或者match_parent則span無效
if (childViewParams.width <= 0) {
span = 1;
}
//如果跨度有變媚污,重新設(shè)置view的寬
if (span > 1 && span <= mColumnNum) {
childViewParams.width = childViewParams.width * span + (span - 1) * mDivider;
} else if (span < 1) {
span = 1;
} else if (span > mColumnNum) {
span = mColumnNum;
childViewParams.width = childViewParams.width * span + (span - 1) * mDivider;
}
//設(shè)置右下左上的邊距
int rightMargin = 0;
int bottomMargin = 0;
int leftMargin = 0;
int topMargin = 0;
//如果跨度+當前的列>設(shè)置的列數(shù),換行
if (span + mCurrentColumn > mColumnNum) {
//換行當前行數(shù)+1
mCurrentRow++;
//當前列等于當前view的跨度
mCurrentColumn = span;
//換行以后肯定是第一個
mRFColPosMap.put(mCurrentRow, i);
//換行操作
//因為換行廷雅,肯定不是第一行
//換行操作后將當前view添加到上一行第一個位置的下面
childViewParams.addRule(RelativeLayout.BELOW, mViewList.get(mRFColPosMap.get(mCurrentRow - 1)).getId());
//不是第一行耗美,所以上邊距為分割線的寬度
topMargin = mDivider;
//換行后位置在左邊第一個,所以左邊距為距離父控件的邊距
leftMargin = mMarginParent;
} else {
if (mCurrentColumn <= 0 && mCurrentRow <= 1) {
//第一行第一列的位置保存第一列信息航缀,同時第一列不需要任何相對規(guī)則
mRFColPosMap.put(mCurrentRow, i);
//第一行第一列上邊距和左邊距都是距離父控件的邊距
topMargin = mMarginParent;
leftMargin = mMarginParent;
} else {
//不是每一行的第一個商架,就添加到前一個的view的右面,并且和前一個頂部對齊
childViewParams.addRule(RelativeLayout.RIGHT_OF, mViewList.get(i - 1).getId());
childViewParams.addRule(ALIGN_TOP, mViewList.get(i - 1).getId());
}
//移動到當前列 mCurrentColumn += span;
}
if (mCurrentColumn >= mColumnNum || i >= mZoomHoverAdapter.getCount() - 1) {
//如果當前列為列總數(shù)或者當前view的下標等于最后一個view的下標那么就是最右邊的view芥玉,設(shè)置父邊距
rightMargin = mMarginParent;
} else {
rightMargin = mDivider;
}
//如果當前view是最后一個那么他肯定是最后一行
if (i >= (mZoomHoverAdapter.getCount() - 1)) {
bottomMargin = mMarginParent;
}
//設(shè)置外邊距
childViewParams.setMargins(leftMargin, topMargin, rightMargin, bottomMargin);
//添加view
addView(childView, childViewParams);
//添加點擊事件
childView.setOnClickListener(this);
}
}
里面的每一行都有注釋蛇摸,就不再這里啰嗦了。
注意:這里如果要用span屬性灿巧,就必須設(shè)置item的寬高赶袄,不能設(shè)置成wrap_content
和match_parent
,因為這兩個獲取LayoutParams后width和Height值是負數(shù)抠藕。
接下來是點擊的邏輯處理:
@Override
public void onClick(View view) {
if (mCurrentView == null) {
//如果currentView為null饿肺,證明第一次點擊
//執(zhí)行放大動畫
zoomInAnim(view);
mCurrentView = view;
if (mOnItemSelectedListener != null) {
mOnItemSelectedListener.onItemSelected(mCurrentView, mCurrentView.getId() - 1);
}
} else {
if (view.getId() != mCurrentView.getId()) {
//點擊的view不是currentView
//currentView執(zhí)行縮小動畫
zoomOutAnim(mCurrentView);
//當前點擊的view賦值給currentView
mCurrentView = view;
//執(zhí)行放大動畫
zoomInAnim(mCurrentView);
if (mOnItemSelectedListener != null) {
mOnItemSelectedListener.onItemSelected(mCurrentView, mCurrentView.getId() - 1);
}
}
}
}
currentView為當前選中的view,邏輯很簡單盾似,不再做解釋了敬辣。
下面是放大動畫的代碼:
/**
* 放大動畫
*
* @param view
*/
private void zoomInAnim(final View view) {
//將view放在其他view之上
view.bringToFront();
//按照bringToFront文檔來的,暫沒測試
if (Build.VERSION.SDK_INT < KITKAT) {
requestLayout();
}
if (mCurrentZoomInAnim != null) {
//如果當前有放大動畫執(zhí)行颜说,cancel掉
mCurrentZoomInAnim.cancel();
}
ObjectAnimator objectAnimatorX = ObjectAnimator.ofFloat(view, "scaleX", 1.0f, mAnimZoomTo);
ObjectAnimator objectAnimatorY = ObjectAnimator.ofFloat(view, "scaleY", 1.0f, mAnimZoomTo);
objectAnimatorX.setDuration(mAnimDuration);
objectAnimatorX.setInterpolator(mZoomInInterpolator);
objectAnimatorY.setDuration(mAnimDuration);
objectAnimatorY.setInterpolator(mZoomInInterpolator);
AnimatorSet set = new AnimatorSet();
set.playTogether(objectAnimatorX, objectAnimatorY);
set.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
//放大動畫開始
if (mOnZoomAnimatorListener != null) {
mOnZoomAnimatorListener.onZoomInStart(view);
}
}
@Override
public void onAnimationEnd(Animator animator) {
//放大動畫結(jié)束
if (mOnZoomAnimatorListener != null) {
mOnZoomAnimatorListener.onZoomInEnd(view);
}
mCurrentZoomInAnim = null;
}
@Override
public void onAnimationCancel(Animator animator) {
//放大動畫退出
mCurrentZoomInAnim = null;
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
set.start();
mCurrentZoomInAnim = set;
}
放大動畫內(nèi)有個關(guān)鍵方法就是view.bringToFront()
,它使view在其他view之上汰聋∶欧啵縮小的動畫代碼就不貼了,寫法和放大類似但是沒有bringToFront()方法烹困,另外還有很多自定義效果的方法等也都不貼了玄妈。
用法
注:為了更靈活本view并沒有加入陰影特效,而是提供了動畫的監(jiān)聽來動態(tài)操作髓梅,這樣可操作性強拟蜻,更加靈活。
完整代碼和用法請看ZoomHoverView