(UPDATE)點擊放大懸浮View的實現(xiàn)之路

UI效果圖

如上圖效果腥刹,公司項目上個版本中需要這個效果拒垃,手機上這種效果幾乎是沒有的雀彼,答對了我們在開發(fā)機頂盒項目纺阔。下面就來說說這個效果的實現(xiàn)路程為了勾引你的欲望瘸彤,先上一下我實現(xiàn)的效果

zoomhover.gif

Changed

  • 最終思路段落:找到了GridLayout調(diào)用bringToFront()不錯亂的方法
  • 具體實現(xiàn)段落:ZoomHoverAdapter的代碼更新
  • 新增繼承GridLayout的實現(xiàn),可以設(shè)置跨行笛钝,跨列

實現(xiàn)思路歷程

第一印象:

開始看這個效果的時候质况,上來的第一印象就是用GridLayout,網(wǎng)格布局迅速搭好了然后測試下玻靡,點擊放大調(diào)用view的setScaleX()和setScaleY()方法拯杠,再在點擊的時候添加一個陰影背景哇!?信潭陪!這不搞定了嘛,運行一下看看效果最蕾。

first time

啊叻叻~發(fā)現(xiàn)放大以后依溯,和其他子view重疊的區(qū)域被遮擋了...WHY?因為所有子view在同一層瘟则,所以就會遮擋黎炉!

項目版本中的實現(xiàn)

由于項目的版本比較緊,所以沒多長時間去研究醋拧,所以當時就采取了FrameLayout+GridLayout+帶陰影的layout 這個方案去實現(xiàn)的慷嗜。FrameLayout在最外層淀弹,GridLayout被包含在其中,還有一個帶有陰影的layout在GridLayout的上層沒點擊的時候GONE掉庆械;在點擊的時候獲取點擊的view然后copy一份到帶陰影l(fā)ayout中薇溃。效果是達到了,但是有缺點:

  1. 修改起來很麻煩缭乘,做不到動態(tài)添加
  2. 增加了布局的層級沐序,每次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(這個名字是我和一位美女一同起的我叫她越越(群里都叫她廢妹)未巫,感謝越越想到這么帥的名字~)窿撬,主要的類有三個:

  1. ZoomHoverAdapter:為ZoomHoverView提供子view(感謝鴻洋大神的FlowLayout項目給我啟發(fā))
  2. ZoomHoverView:繼承自RelativeLayout,setAdapter()后通過adapter去給子view添加布局規(guī)則叙凡,然后addView
  3. 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_contentmatch_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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末枯饿,一起剝皮案震驚了整個濱河市酝锅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌奢方,老刑警劉巖搔扁,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件爸舒,死亡現(xiàn)場離奇詭異,居然都是意外死亡稿蹲,警方通過查閱死者的電腦和手機扭勉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來苛聘,“玉大人涂炎,你說我怎么就攤上這事∩杌” “怎么了勾徽?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長买羞。 經(jīng)常有香客問我峻贮,道長,這世上最難降的妖魔是什么澎粟? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任蛀序,我火速辦了婚禮,結(jié)果婚禮上活烙,老公的妹妹穿的比我還像新娘徐裸。我一直安慰自己,他們只是感情好啸盏,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布重贺。 她就那樣靜靜地躺著,像睡著了一般回懦。 火紅的嫁衣襯著肌膚如雪气笙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天怯晕,我揣著相機與錄音潜圃,去河邊找鬼。 笑死舟茶,一個胖子當著我的面吹牛谭期,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吧凉,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼隧出,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了阀捅?” 一聲冷哼從身側(cè)響起胀瞪,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎饲鄙,沒想到半個月后赏廓,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體涵紊,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年幔摸,在試婚紗的時候發(fā)現(xiàn)自己被綠了摸柄。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡既忆,死狀恐怖驱负,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情患雇,我是刑警寧澤跃脊,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站苛吱,受9級特大地震影響酪术,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜翠储,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一绘雁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧援所,春花似錦庐舟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至滔岳,卻和暖如春杠娱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谱煤。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工摊求, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人趴俘。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓睹簇,卻偏偏與公主長得像奏赘,于是被迫代替她去往敵國和親寥闪。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容