《開發藝術探索中》RevealLayout的一些解釋

注:本文中的代碼是《開發藝術探索》書中的源碼,特別感謝這本書的作者任玉剛先生,真的從其中學到了很多,特此感謝。


先說下實現的效果,就是在這個佈局中的所有控件在點擊時會有一個陰影,手指不離開會從手指的地方出現一個更深的圓形陰影,不斷蔓延到覆蓋整個空間。沒有辦法截圖,如果有魅族手機可以看一下,資訊界面的所用的佈局就是這樣的。

事件分發

先找到被點擊view

public class RevealLayout extends LinearLayout implements Runnable {

    private static final String TAG = "DxRevealLayout";
    private static final boolean DEBUG = true;

    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    private int mTargetWidth;//子view的寬度
    private int mTargetHeight;//子view的高度
    private int mMinBetweenWidthAndHeight;//子view的寬度與高度的最小值
    private int mMaxBetweenWidthAndHeight;//子view的寬度與高度的最大值
    private int mMaxRevealRadius;//半徑最大值
    private int mRevealRadiusGap;//半徑單位變化值
    private int mRevealRadius = 0;//初始半徑
    private float mCenterX;//中心點X
    private float mCenterY;//中心點Y
    private int[] mLocationInScreen = new int[2];//父view的左上角的座標數組

    private boolean mShouldDoAnimation = false;
    private boolean mIsPressed = false;
    private int INVALIDATE_DURATION = 40;

    private View mTouchTarget;//觸摸的view
    private DispatchUpTouchEventRunnable mDispatchUpTouchEventRunnable = new DispatchUpTouchEventRunnable();

    public RevealLayout(Context context) {
        super(context);
        init();
    }

    public RevealLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public RevealLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        setWillNotDraw(false);
        mPaint.setColor(getResources().getColor(R.color.reveal_color));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        this.getLocationOnScreen(mLocationInScreen);
    }

先看一下注釋,這部分內容基本可以理解重寫了父類的構造方法,初始化一些需要的變量。接下來就是重點了。
先重寫父類的dispatchTouchEvent方法來確定事件的分發。

/**
 * 重寫父類的dispatchTouchEvent方法來決定事件的分發
 */
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getRawX();
        int y = (int) event.getRawY();
        int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            View touchTarget = getTouchTarget(this, x, y);
            if (touchTarget != null && touchTarget.isClickable() && touchTarget.isEnabled()) {
                mTouchTarget = touchTarget;
                initParametersForChild(event, touchTarget);
                //TODO???不是很懂
                //查資料意思是用於在工作線程重繪view
                postInvalidateDelayed(INVALIDATE_DURATION);
            }
        } else if (action == MotionEvent.ACTION_UP) {
            mIsPressed = false;
            postInvalidateDelayed(INVALIDATE_DURATION);
            //創建一個執行view點擊事件的任務對象
            mDispatchUpTouchEventRunnable.event = event;
            //把runnable對象放到主線程取執行
            postDelayed(mDispatchUpTouchEventRunnable, 200);
            return true;
        } else if (action == MotionEvent.ACTION_CANCEL) {
            //就是指當前的事件序列沒有執行完的時候執行,被其他的view搶走了
            mIsPressed = false;
            postInvalidateDelayed(INVALIDATE_DURATION);
        }

        return super.dispatchTouchEvent(event);
    }

在這其中有兩個陌生的方法分別作一下說明,getTouchTarget()根據座標獲得點擊位置的veiw,initParametersForChild()用於對初始化的成員進行賦值。

/**
 * 根據座標獲得子view
 * @param view parentview
 * @param x
 * @param y
 * @return
 */
    private View getTouchTarget(View view, int x, int y) {
        View target = null;
        //返回所有能被觸摸的view
        ArrayList<View> TouchableViews = view.getTouchables();
        for (View child : TouchableViews) {
            if (isTouchPointInView(child, x, y)) {
                target = child;
                break;
            }
        }
        return target;
    }

其中又掉用了一個方法isTouchPointInView(),用於判斷座標是否在view的範圍內

/**
 * 根據座標判斷是否在view 中
 * @param view child view 
 * @param x 點擊的x
 * @param y 點擊的Y
 * @return
 */
    private boolean isTouchPointInView(View view, int x, int y) {
        int[] location = new int[2];
        view.getLocationOnScreen(location);
        int left = location[0];
        int top = location[1];
        int right = left + view.getMeasuredWidth();
        int bottom = top + view.getMeasuredHeight();
        if (view.isClickable() && y >= top && y <= bottom
                && x >= left && x <= right) {
            return true;
        }
        return false;
    }

賦值方法

/**
 * 對一系列的屬性進行賦值
 * @param event 事件
 * @param view 被觸摸的view
 */
    private void initParametersForChild(MotionEvent event, View view) {
        mCenterX = event.getX() ;
        mCenterY = event.getY() ;
        mTargetWidth = view.getMeasuredWidth();
        mTargetHeight = view.getMeasuredHeight();
        mMinBetweenWidthAndHeight = Math.min(mTargetWidth, mTargetHeight);
        mMaxBetweenWidthAndHeight = Math.max(mTargetWidth, mTargetHeight);
        mRevealRadius = 0;
        mShouldDoAnimation = true;
        mIsPressed = true;
        mRevealRadiusGap = mMinBetweenWidthAndHeight / 8;
        int[] location = new int[2];
        view.getLocationOnScreen(location);
        //子view的左上角橫座標減去父view的左上角橫座標
        //得到子view相對於父view在水平方向的距離
        int left = location[0] - mLocationInScreen[0];
        //得到點擊點相對於子view左邊的水平距離
        int transformedCenterX = (int)mCenterX - left;
        //返回點擊點在子view中距離水平兩邊的最大值
        mMaxRevealRadius = Math.max(transformedCenterX, mTargetWidth - transformedCenterX);
    }

這樣事件的分發就執行完了,接下來就是需要的重繪了。

重繪

根據獲得點擊事件的view來進行重繪實現點擊的效果。

protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (!mShouldDoAnimation || mTargetWidth <= 0 || mTouchTarget == null) {
            return;
        }
        if (mRevealRadius > mMinBetweenWidthAndHeight / 2) {
            mRevealRadius += mRevealRadiusGap * 4;
        } else {
            mRevealRadius += mRevealRadiusGap;
        }
        this.getLocationOnScreen(mLocationInScreen);
        int[] location = new int[2];
        mTouchTarget.getLocationOnScreen(location);
        int left = location[0] - mLocationInScreen[0];
        int top = location[1] - mLocationInScreen[1];
        int right = left + mTouchTarget.getMeasuredWidth();
        int bottom = top + mTouchTarget.getMeasuredHeight();
        canvas.save();
        canvas.clipRect(left, top, right, bottom);
        canvas.drawCircle(mCenterX, mCenterY, mRevealRadius, mPaint);
        canvas.restore();

        if (mRevealRadius <= mMaxRevealRadius) {
            postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);
        } else if (!mIsPressed) {
            mShouldDoAnimation = false;
            postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);
        }
    }

之前的代碼都能看懂的話這部分應該也不難,就是對屬性的理解和判斷,實現畫一個圓,經常看到的一個方法就是postInvalidateDelayed(),用於延時刷新 view,在源碼中查了查,查到最後也不是很懂最後的方法的實現就是view的刷新機制,之後會繼續關注的。到這裏這個RevealLayout就解釋的差不多了,有些地方我也不是很懂,比如canvas的save和restore方法的作用。mShouldDoAnimation 的作用等。不過如果上面我寫的內容明白了的話,這個佈局也就理解的可以了。(之前說些事件分發的還沒寫,哎,最近好忙啊)。貼一些代碼查找的過程和發現。


下面的內容是對postInvalidateDelayed()方法的追蹤,跟上文沒太大的關係,就是自己記錄一下,個人比較喜歡查找源碼,就好比在暢遊於知識的海洋,最後碰見了一條大魚。
首先是view中的postInvalidateDelayed()方法

/**
     * <p>Cause an invalidate of the specified area to happen on a subsequent cycle
     * through the event loop. Waits for the specified amount of time.</p>
     *
     * <p>This method can be invoked from outside of the UI thread
     * only when this View is attached to a window.</p>
     *
     * @param delayMilliseconds the duration in milliseconds to delay the
     *         invalidation by
     * @param left The left coordinate of the rectangle to invalidate.
     * @param top The top coordinate of the rectangle to invalidate.
     * @param right The right coordinate of the rectangle to invalidate.
     * @param bottom The bottom coordinate of the rectangle to invalidate.
     *
     * @see #invalidate(int, int, int, int)
     * @see #invalidate(Rect)
     * @see #postInvalidate(int, int, int, int)
     */
    public void postInvalidateDelayed(long delayMilliseconds, int left, int top,
            int right, int bottom) {
        // We try only with the AttachInfo because there's no point in invalidating
        // if we are not attached to our window
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            final AttachInfo.InvalidateInfo info = AttachInfo.InvalidateInfo.obtain();
            info.target = this;
            info.left = left;
            info.top = top;
            info.right = right;
            info.bottom = bottom;        attachInfo.mViewRootImpl.dispatchInvalidateRectDelayed(info, delayMilliseconds);
        }
    }

大家稍微看一下就行,就是對一些信息進行封裝到了attachInfo中。重點是最後一句attachInfo.mViewRootImpl.dispatchInvalidateRectDelayed(info,delayMilliseconds);
調用了一個ViewRoot的實現類的dispatchInvalidateRectDelayed()方法;
讓我們看看它裏面做了什麼?

public void dispatchInvalidateRectDelayed(AttachInfo.InvalidateInfo info,
            long delayMilliseconds) {
        final Message msg = mHandler.obtainMessage(MSG_INVALIDATE_RECT, info);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }

可以看到它往消息隊列中發了一個消息。繼續跟蹤下,看看這個消息執行的是什麼?

 @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MSG_INVALIDATE:
                ((View) msg.obj).invalidate();
                break;
            case MSG_INVALIDATE_RECT:
                final View.AttachInfo.InvalidateInfo info = (View.AttachInfo.InvalidateInfo) msg.obj;
                info.target.invalidate(info.left, info.top, info.right, info.bottom);
                info.recycle();
                break;

看case的第二個,執行了一個info.target.invalidate()方法。info.target是什麼哪?我懷疑是一個view對象,取view中查查看有沒有這方法啊?

 /**
     * Mark the area defined by the rect (l,t,r,b) as needing to be drawn.
     * The coordinates of the dirty rect are relative to the view.
     * If the view is visible, {@link #onDraw(android.graphics.Canvas)}
     * will be called at some point in the future. This must be called from
     * a UI thread. To call from a non-UI thread, call {@link #postInvalidate()}.
     * @param l the left position of the dirty region
     * @param t the top position of the dirty region
     * @param r the right position of the dirty region
     * @param b the bottom position of the dirty region
     */
    public void invalidate(int l, int t, int r, int b) {
        if (skipInvalidate()) {
            return;
        }
         if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) ||
                (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID ||
                (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED) {
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            mPrivateFlags |= PFLAG_INVALIDATED;
            mPrivateFlags |= PFLAG_DIRTY;
            final ViewParent p = mParent;
            final AttachInfo ai = mAttachInfo;
            //noinspection PointlessBooleanExpression,ConstantConditions
            if (!HardwareRenderer.RENDER_DIRTY_REGIONS) {
                if (p != null && ai != null && ai.mHardwareAccelerated) {
                    // fast-track for GL-enabled applications; just invalidate the whole hierarchy
                    // with a null dirty rect, which tells the ViewAncestor to redraw everything
                    p.invalidateChild(this, null);
                    return;
                }
            }
            if (p != null && ai != null && l < r && t < b) {
                final int scrollX = mScrollX;
                final int scrollY = mScrollY;
                final Rect tmpr = ai.mTmpInvalRect;
                tmpr.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY);
                p.invalidateChild(this, tmpr);
            }
        }
    }

還真有,看一下都是些什麼東西啊,if判斷,賦值,之後執行了 p.invalidateChild(this, tmpr);這個方法,這p是什麼啊?點一下方法,就會跳到這裏。

 /**
     * All or part of a child is dirty and needs to be redrawn.
     * 
     * @param child The child which is dirty
     * @param r The area within the child that is invalid
     */
    public void invalidateChild(View child, Rect r);

這是ViewParent接口中的一個抽象方法,到這裏,跟蹤就暫時進行不下去了,因爲我們並不知道需要到哪個實現了這個接口的實現類中去查看實現的方法,考慮一下,方法中的child是誰?是不是我們最開始確定的被點擊的view,那這個view的父類是誰哪?對,就是RevealLayout,現在回到我們的RevealLayout中去看看有沒有這個方法。會發現並沒有實現接口,那RevealLayout的父類LinearLayout那,我們看下。

@RemoteView
public class LinearLayout extends ViewGroup {
public abstract class ViewGroup extends View implements ViewParent, ViewManager 

好的現在我們找到了這個invalidateChild()方法的實現對象了,去ViewGroup 中看看吧。

   /**
     * Don't call or override this method. It is used for the implementation of
     * the view hierarchy.
     */
    public final void invalidateChild(View child, final Rect dirty) {
        ViewParent parent = this;

這個方法裏有很多的判定,賦值之類的,一時間我是看不過來,有能力的可以繼續查一下。主要的我就是想找到在哪裏調用了RevealLayout中的dispatchDraw這個方法,不過也沒有找到,我之前說的大魚就是這個方法,還有一個跟它在一起的

/**
     * Don't call or override this method. It is used for the implementation of
     * the view hierarchy.
     *
     * This implementation returns null if this ViewGroup does not have a parent,
     * if this ViewGroup is already fully invalidated or if the dirty rectangle
     * does not intersect with this ViewGroup's bounds.
     */
    public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {

這兩個方法實現了對viewgroup中的子view進行刷新操作,這種代碼塊實在是太多了,只能通過註釋來稍微理解一點。不過還是沒有找到我想要的,因爲view的刷新肯定是要實時進行的,也就是說dispatchDraw這個方法會執行很多次,直到結束,那麼在刷新的時候哦是誰在執行這個方法?在什麼時候執行?有感興趣的可以留言或私信相互交流,之後也會關注一下view刷新機制流程方面的信息。那麼這篇博客到這就結束了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章