自定義卡片效果的ViewGroup

不逼逼,看效果!兩邊有點露出來的效果,比如騰訊視頻App的上方的效果,都是輕量級的控件,請勿見怪,總體時間花費大約9個小時,其中找Bug找了3個小時,哈哈!

微信圖片_20170904213432.jpg
  • 第一個效果是正常的滑動情況


    xiao.gif
  • 第二個效果是禁止滑動情況,同時呢,有一個回彈的效果,四川話講這個很巴適


    xiao.gif

分享兩個東西

  • 今天發現的一個Android UI 開發效率的 UI 庫:https://github.com/QMUI/QMUI_Android
  • 這個我都不好意思分享,嘿嘿,周天就做這個,做完了發現根本沒有什么東西可以分享,所以就寫了現在這個博客,等我以后研究下hexo,才來更新

寫在前面的話:如果我手寫慢一點,多看看一下api,我就不會把兩個api寫錯了,由于手滑寫錯了,導致我這篇博客現在才來寫,興奮感都快磨完了。

72F75ABC3304DD06A39EB5A18180F6CE.gif
  • 這輩子我都不會忘記這個值了,getScaledTouchSlop()是一個距離,表示滑動的時候,手的移動要大于這個距離才開始移動控件。viewpager就是用這個距離來判斷用戶是否翻頁,只不過呢
ViewConfiguration.get(mContext).getScaledTouchSlop()

原理如下: mTouchSlop = configuration.getScaledPagingTouchSlop();就是這個值,但是你可能會說有毛的的關系啊,別急



往ViewConfiguration類看記住這個值


image.png

看這個值mTouchSlop,對吧只不過在ViewPager判斷是否需要移動的時候,這個距離是*2。

image.png

由于我這里需要更高的精度,所以獲取了這個值getScaledTouchSlop()


  • 可千萬不要拿到getScaledDoubleTapSlop()這個值了啊!
//第一次觸摸和第二次觸摸之間的距離,Distance in pixels between the first touch and second touch
   ViewConfiguration.get(mContext).getScaledDoubleTapSlop();

繼承ViewGroup,重寫構造方法

public class CardViewPager extends ViewGroup{

 public CardViewPager(Context context) {
        this(context,null);
    }

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

初始化init

   private void init(Context context) {
        mContext = context;
        //滑動的對象
        mScroller = new Scroller(mContext);
        //getScaledTouchSlop是一個距離,表示滑動的時候,手的移動要大于這個距離才開始移動控件。
        // 如果小于這個距離就不觸發移動控件,如viewpager就是用這個距離來判斷用戶是否翻頁
//        mScaledDoubleTapSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
        mScaledDoubleTapSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop();
        //第一次觸摸和第二次觸摸之間的距離,Distance in pixels between the first touch and second touch
        ViewConfiguration.get(mContext).getScaledDoubleTapSlop();
        FIRST_width = dp2px(mContext, 10);
        TWO_GAP_WIDTH = FIRST_width * 2;
        THREE_GAP_WIDTH = FIRST_width * 3;
        FOUR_GAP_WIDTH = FIRST_width * 4;
    }

onMeasure,重寫測量這里記住widthMeasureSpec、heightMeasureSpec是一個32位的int值,其中高兩位是物理模式,低的30位才是控件的寬度和高度的信息。

MeasureSpec.EXACTLY:父視圖希望子視圖的大小應該是specSize中指定的。
MeasureSpec.AT_MOST:子視圖的大小最多是specSize中指定的值,也就是說不建議子視圖的大小超過specSize中給定的值。
MeasureSpec.UNSPECIFIED:我們可以隨意指定視圖的大小。

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //這才是真正的寬度和高度
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heighSize = MeasureSpec.getSize(heightMeasureSpec);
        //設置測量的大小
        setMeasuredDimension(widthSize,heighSize);
        //測量孩子的大小
        mChildCount = getChildCount();
        for (int i=0;i<mChildCount;i++){
            //這里需要把模式也傳入進去
            getChildAt(i).measure(widthMeasureSpec,heightMeasureSpec);
        }
    }

onLayout重新布局:將孩子的view布局,這里橫向的布局,一個字View接著右邊,這是設計之初的方法,自己先明白到底是怎么樣布局,就好像我明白的方式,是個偉大的ui妹子說,看這個app,就是這樣,哈哈

/**
     * @param changed
     * @param l 左上角的left
     * @param t top
     * @param r  右下角right
     * @param b bottom值
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        View child;
        int widthLeft=0;
        for (int i=0;i<mChildCount;i++){
            child = getChildAt(i);
            //得到第一個孩子的寬度,兩邊都減去了兩個參數,記住是這個4倍值
            int measuredWidth = child.getMeasuredWidth() - FOUR_GAP_WIDTH;
            int measuredHeight = child.getMeasuredHeight();
            //是第一個孩子
            if (i==0){
                child.layout(widthLeft+TWO_GAP_WIDTH,0,widthLeft+TWO_GAP_WIDTH+measuredWidth,measuredHeight);
                //改變向左的值
                widthLeft+=measuredWidth+THREE_GAP_WIDTH;
            }else {
                child.layout(widthLeft, 0, widthLeft + measuredWidth, measuredHeight);
                widthLeft += measuredWidth + FIRST_width;
            }
        }
    }

效果雖然看了,但是真正理解的layout的話,還需明白其中的原理,這里我不講了的太細,獻上美圖一張,嗦嘎,原理就是,不是每一個屏幕都在裝著一個我們的卡片,我們每次移動的時候,也不是移動一個屏幕,而是通過運算的方式,移動到恰好能夠看到兩邊10dp的值,這里的要轉成像素

 dp2px(mContext, 10);

image.png

關于像素px我還想說說: context.getResources().getDisplayMetrics().density;density顯示器的邏輯密度,這是【獨立的像素密度單位(首先明白dp是個單位)】的一個縮放因子,在屏幕密度大約為160dpi的屏幕上,一個dp等于一個px,這個提供了系統顯示器的一個基線. 例如:屏幕為240320的手機屏幕,其尺寸為 1.5"2" 也就是1.5英寸乘2英寸的屏幕 它的dpi(屏幕像素密度,也就是每英寸的像素數,dpi是dot per inch的縮寫)大約就為160dpi, 所以在這個手機上dp和px的長度(可以說是長度,最起碼從你的視覺感官上來說是這樣的)是相等的。 因此在一個屏幕密度為160dpi的手機屏幕上density的值為1,而在120dpi的手機上為0.75等等.例如:一個240320的屏幕盡管他的屏幕尺寸為1.8"1.3",(我算了下這個的dpi大約為180dpi多點)但是它的density還是1(也就是說取了近似值) 然而,如果屏幕分辨率增加到320480 但是屏幕尺寸仍然保持1.5"2" 的時候(和最開始的例子比較)
這個手機的density將會增加(可能會增加到1.5)

 public static int dp2px(Context context, float dpValue) {
        ///這個得到的不應該叫做密度,應該是密度的一個比例。不是真實的屏幕密度,
        /// 而是相對于某個值的屏幕密度。也可以說是相對密度
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

攔截事件:當大于了需要移動控件的距離的話,就需要把這個事件攔截自己處理。

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                int x = (int) ev.getX();
                mLastMotionX = x ;
                break;
            case MotionEvent.ACTION_MOVE:
                x= (int) ev.getX();
                //滑動的距離
                int delX = mLastMotionX - x;
                //如果說距離大于這個距離的話,就需要滾動了,攔截事件
                if (Math.abs(delX)>mScaledDoubleTapSlop){
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

處理事件:在down的事件一定需要攔截,才能記錄坐標

返回值為True,代表攔截這次事件,直接進入到ViewGroup的onTouchEvent中,就不會進入到View的onTouchEvent了
返回值為False,代表不攔截這次事件,不進入到ViewGroup的onTouchEvent中,直接進入到View的onTouchEvent中

 public boolean onTouchEvent(MotionEvent event) {
        //如果沒有孩子的話,不需要攔截
        if (getChildCount()==0){
            return false;
        }
        //監聽滑動的速度
        obtainTracker(event);
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()){
                    //停止動畫
                    mScroller.abortAnimation();
                }
                int x = (int) event.getX();
                mLastMotionX=x;
                //不管怎么怎么樣這個事件都必須攔截
                return true;

            case MotionEvent.ACTION_MOVE:
                 x = (int) event.getX();
                int desX = mLastMotionX - x;
                //這個距離大于了屏幕的10/1的話,就給他賦值10/1
                if (!isAllowScroll&&desX>getWidth()/10){
                    desX=getWidth()/10;
                    //如果設置了不可以滑動的,這個flag需要到up事件單獨處理
                    mCanScrolled = true;
                }
                //只需計算x的距離
                mVelocityTracker.computeCurrentVelocity(1000,ViewConfiguration.getMaximumFlingVelocity());
                mXVelocity = (int) mVelocityTracker.getXVelocity();
                //如果說距離滑動太小,或者是只有一個屏幕的話,就不往下去做操作了
                if (Math.abs(desX)<mScaledDoubleTapSlop||(desX>=0&&
                        mCurScreen==mChildCount-1)||(desX<=0&&mCurScreen==0)){
                    break;
                }

                //能到這里來的話,就必須往手指方向慢慢滾動了
                scrollTo(getChildAt(mCurScreen).getLeft()+desX,0);
                break;
            case MotionEvent.ACTION_UP:
                //mXVelocity為正數的話,這個是往left滾
                if (isAllowScroll&&mXVelocity>MAX_VELOCITY_VALUE&&mCurScreen>0){
                    scrollScreen(mCurScreen-1);
                }else if (isAllowScroll&&mXVelocity<-MAX_VELOCITY_VALUE&&mCurScreen<mChildCount-1){
                    scrollScreen(mCurScreen+1);
               //當設置了不能滑動時候,并且手指滑動的Move的距離已經超過了屏幕的10/1,有一個回彈的效果,左右搖擺
                }else if (mCanScrolled){
                    springToDestination();
                }else{
                    snapToDestination();
                }
                //最后不要忘記了釋放
                releaseVelocityTracker();
                break;
        }
        return super.onTouchEvent(event);
    }

滑動到指定的屏幕,在記住兩個地方,就不需要滑動了,一個是在最右和最左端。

 private void scrollScreen(int whichScreen) {
        //防止超出了最大的孩子的數量
        int min = Math.min(whichScreen, mChildCount - 1);
        whichScreen = Math.max(0, min);
        //getScrollX() 就是當前view的左上角相對于母視圖的左上角的X軸偏移量。
        //在這里當getScrollX==0的時候,等于后面的whichScreen*getWidth()那么就滑動到第一頁了
        //后續就不需要滑動了,也不需要重新繪制了
        // TODO: 2017/9/3 這里只在最左不能進去滑動了,其實在最右端也是不能夠去滑動了,帶解決
        if (getScrollX()!=whichScreen*getWidth()){
            int deltaX = whichScreen * (getWidth() - THREE_GAP_WIDTH) - getScrollX();
            mCurScreen = whichScreen;
            mScroller.startScroll(getScrollX(), 0, deltaX, 0, Math.abs(deltaX));
            invalidate();
        }
    }

但是在這里我留下一個問題,但是在這里我留下一個問題在我的手機上我測試了1到5個孩子的情況分別數據如下:
getScrollX()和whichScreen*getWidth()
1個屏幕是0 0--------0
2個屏幕是90 990 -------1080
3個屏是 120
4個屏 =270 2970 -------3240
5個屏 =380 3960 -------4320
當我們滑動到最有端的時候,其實也是不能夠去滑動了,但是我這個方法呢是能夠 走到if當中的,而且對應關系也不太明確,這個問題我還得想想。

還需要理解一個東西getScrollX()到底是什么值?再次獻上我的美作,哈哈,反正我是明白了,我怕講不明白,所以先看圖,然后抓日記,一下就明白!

image.png

監聽滑動的速度,在上篇筆鋒效果里面有講到過,還是Viewpager里面的東西,記住要釋放這個算是監聽吧!

     //監聽滑動的速度
        obtainTracker(event);

    private void obtainTracker(MotionEvent event) {
        if (mVelocityTracker==null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        //綁定事件
        mVelocityTracker.addMovement(event);
    }
  /**
     * 釋放監聽滑動速度方法
     */
    private void releaseVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

其實你就把上面的工作全部都做好了,你會發現還是不能夠翻頁,來吧去Viewpager看看,再去度娘看看

  /**
     * 計算滾動的位置
     */
    @Override
    public void computeScroll() {
        super.computeScroll();
        //返回值為boolean,mScroller.computeScrollOffset()==true說明滾動尚未完成,false說明滾動已經完成。
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }

    }

image.png

個人翻譯就是在viewpager中需要重新計算滑動的位置


image.png

回彈滑動目的屏,這里就是需要有點動畫的效果,先回到原來的位置,然后左右搖擺搖擺!

/**
     * 回彈滑動目的屏
     */
    private void springToDestination() {
        System.out.println("shiming  ==springToDestination");
        int screenWidth = getWidth();
        int whichScreen = (getScrollX() + screenWidth / 2) / screenWidth;
        whichScreen = Math.max(0, Math.min(whichScreen, mChildCount - 1));
        final int deltaX = whichScreen * (getWidth() - THREE_GAP_WIDTH) - getScrollX();
        mCurScreen = whichScreen;
        //先給我滾動到原來的位置
        springToScroll(deltaX * 1.0f, Math.abs(deltaX));
        //向右的給我擺動兩下
        postDelayed(new Runnable() {
            @Override
            public void run() {
                springToScroll(-deltaX * 0.3f, Math.abs(deltaX));
            }
        }, Math.abs(deltaX));
        //讓后給我向左擺動兩下
        postDelayed(new Runnable() {
            @Override
            public void run() {
                springToScroll(deltaX * 0.3f, Math.abs(deltaX));
            }
        }, Math.abs(deltaX * 2));
        mCanScrolled = false;
    }

 /**
     *  getScrollX() 水平方向滾動的偏移值,以像素為單位。正值表明滾動將向左滾動
       startY 垂直方向滾動的偏移值,以像素為單位。正值表明滾動將向上滾動
       (int) deltaX 水平方向滑動的距離,正值會使滾動向左滾動
       0  垂直方向滑動的距離,正值會使滾動向上滾動
     * @param deltaX
     * @param duration
     */
    private void springToScroll(float deltaX, int duration) {
        mScroller.startScroll(getScrollX(), 0, (int) deltaX, 0, duration);
        invalidate();
    }

好了,以上,我寫的寫的都要睡早了,代碼有些注釋還比較詳細一點,如有需要看代碼吧,由于工程邏輯上,還有很多復雜的代碼,我這里就提取了一部分,以供學習之用,謝謝!

地址Git:https://github.com/Shimingli/CardViewPager

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容