Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義ScaleLayout (模仿小米相冊查看圖片效果)

自定義ScaleLayout (模仿小米相冊查看圖片效果)

編輯:關於Android編程

github地址:https://github.com/niniloveyou/ScaleLayout

下面會從以下幾個方面分析如何實現這個效果:

1.初始化完成後做了什麼

2.onMeasure onLayout

3.觸摸事件的處理

4.對外提供方法和接口.

首先講講大概的思路:
就是我們要有三個View 分別為TopView CenterView bottomView 這很好理解,故名思義就是把這三個子View分別放在ViewGroup的上中下。
OnMeasure()中把CenterView的大小設置為等同於自身的大小
onLayout() 獲取topview bottomView的高度,根據高度設置當centerView縮小時topView/BottomView位移距離
onInterceptTouchEvent() 只處理滑動沖突部分。
onTouchEvent()中才是真正滑動縮小或放大實現的部分。

1.初始化完成後做了什麼

我們先貼代碼,後面緊跟著解釋:

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        int childCount = getChildCount();
        if(childCount 

2.onMeasure onLayout

 /**
     * 使得centerView 大小等同ScaleLayout的大小
     * 如果不想這樣處理,也可以在觸摸事件中使用TouchDelegate
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
        int layoutWidth = widthSize - getPaddingLeft() - getPaddingRight();

        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(layoutWidth, MeasureSpec.EXACTLY);
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(layoutHeight, MeasureSpec.EXACTLY);

        mCenterView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }


    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        if(mBottomView != null){
            mBottomViewMoveDistance = mBottomView.getMeasuredHeight();
        }

        if(mTopView != null){
            mTopViewMoveDistance = mTopView.getMeasuredHeight();
        }

        if(mSuggestScaleEnable){
            setMinScale(getSuggestScale());
        }
    }

很簡單,只說一點:mBottomViewMoveDistance, mTopViewMoveDistance 分別為bottomView, topView動畫時位移的距離。

3.觸摸事件的處理

重點來了這個也是核心部分了。

onInterceptTouchEvent

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        boolean intercept = false;

        switch (ev.getAction()) {

            case MotionEvent.ACTION_DOWN:

                onTouchEvent(ev);
                mInitialMotionX = ev.getX();
                mInitialMotionY = ev.getY();
                break;

            case MotionEvent.ACTION_MOVE:
                final float deltaX = Math.abs(ev.getX() - mInitialMotionX);
                final float deltaY = Math.abs(ev.getY() - mInitialMotionY);

                if(mCanScaleListener != null
                        && !mCanScaleListener.onGetCanScale(ev.getX() - mInitialMotionX > 0)){
                    intercept = false;
                }else {
                    intercept = deltaY > deltaX && deltaY > mTouchSlop;
                }
                break;
        }
        return intercept;
    }

所有的down事件都不攔截,因此接下來的move, up事件,
都會先執行onInterceptTouchEvent的(move, up)
繼而分發給子view的dispatchTouchEvent(move, up),
因此在onInterceptTouchEvent(move)事件中我們可以判斷是否滿足滑動條件,滿足就攔截,攔截了之後move up事件就會都分發給自身的OnTouchEvent, 否則如上繼續分發給子View.

intercept = deltaY > deltaX && deltaY > mTouchSlop;

即Y位移的距離大於X方向 ,並且Y方向位移的距離大於TouchSlop,則認為這是有效滑動。

 /**
     * 返回是否可以scale,主要為了適配部分有滑動沖突的view
     * 如TouchImageView, 甚至webView等
     * isScrollSown = true  代表向下,
     * isScrollSown = false 代表向上
     */
    public interface OnGetCanScaleListener{

        boolean onGetCanScale(boolean isScrollSown);
    }
if(mCanScaleListener != null 
          && !mCanScaleListener.onGetCanScale(ev.getX() - mInitialMotionX > 0)){ 
  intercept = false;
}

這下明白了吧,我是做了個接口,要不要攔截由你說了算,也算我偷懶了。

OnTouchEvent

 /**
     * 該方法中實現了
     * 上滑縮小下滑放大功能
     * 也可設置為 上滑放大下滑縮小
     * @param ev
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent ev) {

        if (!isEnabled() || !mSlideScaleEnable) {
            return super.onTouchEvent(ev);
        }



        switch (ev.getActionMasked()) {

            case MotionEvent.ACTION_DOWN:
                downY = ev.getY();
                return true;

            case MotionEvent.ACTION_MOVE:
                if(mCanScaleListener != null && !mCanScaleListener.onGetCanScale(ev.getY() - downY > 0)){
                    return super.onTouchEvent(ev);
                }
                if (Math.abs(ev.getY() - downY) > mTouchSlop) {

                    mSlopLength += (ev.getY() - downY);

                    float scale;
                    if (mSlideUpOrDownEnable) {

                        scale = 1 + (0.8f * mSlopLength / getMeasuredHeight());
                    } else {
                        scale = 1 - (0.8f * mSlopLength / getMeasuredHeight());
                    }

                    scale = Math.min(scale, 1f);

                    mCurrentScale = Math.max(mMinScale, scale);

                    doSetScale();

                    downY = ev.getY();
                }

                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (mCurrentScale > mMinScale && mCurrentScale < 1f) {

                    float half = (1 - mMinScale) / 2;

                    if (mCurrentScale >= mMinScale + half) {

                        setState(STATE_CLOSE, true);
                    } else {

                        setState(STATE_OPEN, true);
                    }
                }
                break;
        }

        return super.onTouchEvent(ev);
    }

這部分,首先是move的時候用mSlopLength計算滑動的距離向下滑就加正值,向上劃值就減小,不斷根據這個值計算當前的Scale. 應該縮放的比例,然後根據這個值計算topView bottomView 的透明度,位移距離,等等, 當UP的時候,根據當前的Scale決定是應該放大到原寬高還是縮小,以動畫的形式。

···
/**
* 1.觸發監聽事件
* 2.計算scale的pivotX, pivotY(因為topView 和bottomView 的高度可能不一樣,所以不能固定設置在中心點)
* 3.設置 mCenterView的scale
* 4.設置topView and BottomView 的動畫(漸變和位移)
*/
private void doSetScale() {

    int scaleListenerCount = mScaleListenerList.size();

    OnScaleChangedListener mScaleChangedListener;
    for (int i = 0; i < scaleListenerCount; i++) {
        mScaleChangedListener = mScaleListenerList.get(i);
        if(mScaleChangedListener != null){
            mScaleChangedListener.onScaleChanged(mCurrentScale);
        }
    }

    if(mCurrentScale == mMinScale || mCurrentScale == 1f){
        int stateListenerCount = mStateListenerList.size();

        OnStateChangedListener mStateChangedListener;
        for (int i = 0; i < stateListenerCount; i++) {
            mStateChangedListener = mStateListenerList.get(i);
            if(mStateChangedListener != null){
                mStateChangedListener.onStateChanged(mCurrentScale == mMinScale);
            }
        }
    }

    doSetCenterView(mCurrentScale);
    doSetTopAndBottomView(mCurrentScale);
}

···

我把監聽事件也貼上:

    /**
     * 當centerView 的scale變化的時候,通過這個
     * 接口外部的View可以做一些同步的事情,
     * 比如,你有一個其他的view要根據centerView的變化而變化
     */
    public interface OnScaleChangedListener{

        void onScaleChanged(float currentScale);
    }

    /**
     * state == false 當完全關閉(scale == 1f)
     * state == true  或當完全開啟的時候(scale = mMinScale)
     */
    public interface OnStateChangedListener{

        void onStateChanged(boolean state);
    }

4.對外提供方法和接口

關於接口,代碼我都無恥的貼上去了。

下面說說提供的幾個簡單的外部方法:

    /**
     * 設置最小scale
     * {@link #DEFAULT_MIN_SCALE}
     * @param minScale
     */
    public void setMinScale(float minScale){

        if(minScale > 0f && minScale < 1f){
            if(mMinScale != minScale){
                if(isOpen()){
                    if(animator != null){
                        animator.cancel();
                        animator = null;
                    }
                    animator = getAnimator(mMinScale, minScale);
                    animator.start();
                }
                mMinScale = minScale;
            }
        }
    }


    public float getMinScale(){
        return mMinScale;
    }

    public float getCurrentScale(){
        return mCurrentScale;
    }


    public void setSuggestScaleEnable(boolean enable){
        if(mSuggestScaleEnable != enable){
            mSuggestScaleEnable = enable;
            requestLayout();
        }
    }

    /**
     * 設置的scale不得當的話,有可能topView / bottomView被覆蓋
     * 通過設置{@link #setSuggestScaleEnable(boolean)}啟用
     * @return
     */
    private float getSuggestScale(){

        int height = 0;

        if(mTopView != null){
            height += mTopView.getMeasuredHeight();
        }

        if(mBottomView != null){
            height += mBottomView.getMeasuredHeight();
        }
        return 1 - height * 1f / (getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
    }


    /**
     * 設置是否啟用滑動縮小功能
     * @param enable
     */
    public void setSlideScaleEnable(boolean enable){
        this.mSlideScaleEnable = enable;
    }

    /**
     *   現在有這麼幾種情況, 默認第二種, 兩者都可以的話,感覺好奇怪,
     *   比如一直下滑會由大變小後又變大,操作感覺不是很好
     *   1. 只上滑放大下滑縮小  false
     *   2. 只上滑縮小下滑放大  true
     */
    public void setSlideUpOrDownEnable(boolean enable){
        this.mSlideUpOrDownEnable = enable;
    }

    /**
     * add OnScaleChangedListener
     * @param listener
     */
    public void addOnScaleChangedListener(OnScaleChangedListener listener){
        if(listener != null){
            mScaleListenerList.add(listener);
        }
    }

    /**
     * add OnStateChangedListener
     * @param listener
     */
    public void addOnStateChangedListener(OnStateChangedListener listener){
        if(listener != null){
            mStateListenerList.add(listener);
        }
    }

    public void setOnGetCanScaleListener(OnGetCanScaleListener listener){
        mCanScaleListener = listener;
    }

    /**
     *  {@link #setState(int state, boolean animationEnable)}
     * @param state
     */
    public void setState(int state){
        setState(state, true);
    }

    /**
     * 設置狀態變化
     * @param state open or close
     * @param animationEnable change state with or without animation
     */
    public void setState(final int state, boolean animationEnable) {

        if(!animationEnable)
        {
            if(state == STATE_CLOSE){
                mSlopLength = 0;
                mCurrentScale = 1;
            }else{
                if(mSlideUpOrDownEnable) {
                    mSlopLength = -getMeasuredHeight() * (1 - mMinScale) * 1.25f;
                }else{
                    mSlopLength = getMeasuredHeight() * (1 - mMinScale) * 1.25f;
                }
                mCurrentScale = mMinScale;
            }
            doSetScale();
            mState = state;

        }else{
            if(animator != null){
                animator.cancel();
                animator = null;
            }

            if(state == STATE_CLOSE && mCurrentScale != 1){

                mSlopLength = 0;
                animator = getAnimator(mCurrentScale, 1f);

            }else if(state == STATE_OPEN && mCurrentScale != mMinScale){

                if(mSlideUpOrDownEnable) {
                    mSlopLength = -getMeasuredHeight() * (1 - mMinScale) * 1.25f;
                }else{
                    mSlopLength = getMeasuredHeight() * (1 - mMinScale) * 1.25f;
                }
                animator = getAnimator(mCurrentScale, mMinScale);
            }

            if(animator != null) {
                animator.addListener(new AnimatorListenerAdapter() {

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        mState = state;
                    }

                });
                animator.start();
            }
        }
    }

    /**
     * 獲取當前狀態開啟或者關閉
     * @return
     */
    public boolean isOpen(){

        return mState == STATE_OPEN;
    }

代碼貼完了。

如果感覺還行,到我的github star一下吧。 謝謝!

https://github.com/niniloveyou/ScaleLayout

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved