【Android View事件(三)】Scroll類源碼分析與應(yīng)用


【本文出自大圣代的技術(shù)專欄 http://blog.csdn.net/qq_23191031
【禁止任何商業(yè)活動(dòng)。轉(zhuǎn)載煩請(qǐng)注明出處】

學(xué)前準(zhǔn)備

詳解Android控件體系與常用坐標(biāo)系
Android常用觸控類分析:MotionEvent 、 ViewConfiguration、VelocityTracker

前言

在前面的幾篇文章,我向大家介紹的都是單一View事件,從這篇文章開始,我將向大家介紹連續(xù)的事件 —— 滑動(dòng)?;瑒?dòng)是移動(dòng)端設(shè)備提供的重要功能,正是由于強(qiáng)大的滑動(dòng)事件讓我們小巧的屏幕可以展現(xiàn)無(wú)限的數(shù)據(jù)。而滑動(dòng)事件沖突卻常常困擾著廣大開發(fā)者。孫子云:知己知彼,百戰(zhàn)不殆。想更好的協(xié)調(diào)滑動(dòng)事件,不知道其中原理的確困難重重。當(dāng)你學(xué)習(xí)本篇文章之后你會(huì)發(fā)現(xiàn)其實(shí)Scroll很簡(jiǎn)單,你只是被各種文章與圖書弄糊涂了。

在真正講解之前,我們需要掌握Android坐標(biāo)系與觸控事件相關(guān)知識(shí),對(duì)此不太明確的同學(xué)請(qǐng)參見上文的 學(xué)前準(zhǔn)備

View滑動(dòng)產(chǎn)生的原理

從原理上講View滑動(dòng)的本質(zhì)就是隨著手指的運(yùn)動(dòng)不斷地改變坐標(biāo)。當(dāng)觸摸事件傳到View時(shí),系統(tǒng)記下觸摸點(diǎn)的坐標(biāo),手指移動(dòng)時(shí)系統(tǒng)記下移動(dòng)后的觸摸的坐標(biāo)并算出偏移量,并通過(guò)偏移量來(lái)修改View的坐標(biāo),不斷的重復(fù)這樣的過(guò)程,從而實(shí)現(xiàn)滑動(dòng)過(guò)程。

1 scrollTo 與 scrollBy

說(shuō)到Scroll就不得不提到scrollTo()與scrollBy()這兩個(gè)方法。

1.1 scrollTo

首先我們要知道Android每一個(gè)控件都有滾動(dòng)條,只不過(guò)系統(tǒng)對(duì)我們隱藏了,所以我們看不見。
對(duì)于控件來(lái)說(shuō)它的大小是有限的,(例如我們指定了大小、屏幕尺寸的束縛等),系統(tǒng)在繪制圖像的時(shí)候只會(huì)在這個(gè)有限的控件內(nèi)繪制,但是內(nèi)容(content)的載體Canvas在本質(zhì)上是無(wú)限的,例如我們的開篇圖片,控件仿佛就是一個(gè)窗口我們只能通過(guò)它看到這塊畫布。

    /**
     * Set the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    public void scrollTo(int x, int y) {//滾動(dòng)到目標(biāo)位置
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX; // 已經(jīng)滾動(dòng)到的X
            int oldY = mScrollY; //已經(jīng)滾動(dòng)到的Y
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);//回調(diào)方法,通知狀態(tài)改變
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation(); //重新繪制
            }
        }
    }

通過(guò)注釋Set the scrolled position of your view我們可以清楚的得知 scrollTo(x,y)的作用就是將View滾動(dòng)到(x,y)這個(gè)點(diǎn),注意是滾動(dòng)(scroll本意滾動(dòng),滑動(dòng)是translate)。

在初始時(shí) mScrollX 與mScrollY均為0,表示著View中展示的是從畫布左上角開始的內(nèi)容(如圖 1),當(dāng)調(diào)用scrollTo(100,100)時(shí)相當(dāng)于將View的坐標(biāo)原點(diǎn)滾動(dòng)到(100,100)這個(gè)位置,展示畫布上從(100,100)開始的內(nèi)容(如圖2),但是事實(shí)上View是靜止不動(dòng)的,所以最終的效果是View的內(nèi)容平移了(-100,-100)的偏移量(如圖3)

image.png

1.2 scrollBy

/** 
    * Move the scrolled position of your view. This will cause a call to 
    * {@link #onScrollChanged(int, int, int, int)} and the view will be 
    * invalidated. 
    * @param x the amount of pixels to scroll by horizontally 
    * @param y the amount of pixels to scroll by vertically 
    */  
   public void scrollBy(int x, int y) {  
       scrollTo(mScrollX + x, mScrollY + y);  
   }  

學(xué)習(xí)scrollTo在學(xué)習(xí)scrollBy就簡(jiǎn)單了,通過(guò)源碼可以看到它里面調(diào)用了ScrollTo(),傳入的參數(shù)是mScrollX+x,也就是說(shuō)這次x是一個(gè)增量,所以scrollBy實(shí)現(xiàn)的效果就是,在當(dāng)前位置上,再偏移x距離
這是ScrollTo()和ScrollBy()的重要區(qū)別。

1.3 小結(jié):

  1. scrollTo與scrollBy都會(huì)另View立即重繪,所以移動(dòng)是瞬間發(fā)生的
  2. scrollTo(x,y):指哪打哪,效果為View的左上角滾動(dòng)到(x,y)位置,但由于View相對(duì)與父View是靜止的所以最終轉(zhuǎn)換為相對(duì)的View的內(nèi)容滑動(dòng)到(-x,-y)的位置。
  3. scrollBy(x,y): 此時(shí)的x,y為偏移量,既在原有的基礎(chǔ)上再次滾動(dòng)
  4. scrollTo與scrollBy的最用效果會(huì)作用到View的內(nèi)容,所以要是想滑動(dòng)當(dāng)前View,就需要對(duì)其父View調(diào)用二者。也可以在當(dāng)前View中使用((View)getParent).scrollXX(x,y)達(dá)到同樣目的。

2 Scroller

OK,通過(guò)上面的學(xué)習(xí)我們知道scrollTo與scrollBy可以實(shí)現(xiàn)滑動(dòng)的效果,但是滑動(dòng)的效果都是瞬間完成的,在事件執(zhí)行的時(shí)候平移就已經(jīng)完成了,這樣的效果會(huì)讓人感覺突兀,Google建議使用自然過(guò)渡的動(dòng)畫來(lái)實(shí)現(xiàn)移動(dòng)效果。因此,Scroller類這樣應(yīng)運(yùn)而生了。

2.1 簡(jiǎn)單實(shí)例

舉一個(gè)簡(jiǎn)單的實(shí)例方便大家的理解與學(xué)習(xí) Scroller

主要代碼

public class CustomScrollerView extends LinearLayout {
    private Scroller mScroller;

    private View mLeftView;
    private View mRightView;

    private float mInitX, mInitY;
    private float mOffsetX, mOffsetY;

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

    public CustomScrollerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomScrollerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        this.setOrientation(LinearLayout.HORIZONTAL);

        mScroller = new Scroller(getContext(), null, true);
    }

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

        if (getChildCount() != 2) {
            throw new RuntimeException("Only need two child view! Please check you xml file!");
        }

        mLeftView = getChildAt(0);
        mRightView = getChildAt(1);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                mInitX = ev.getX();
                mInitY = ev.getY();
                super.dispatchTouchEvent(ev);
                return true;
            case MotionEvent.ACTION_MOVE:
                //>0為手勢(shì)向右下
                mOffsetX = ev.getX() - mInitX;
                mOffsetY = ev.getY() - mInitY;
                //橫向手勢(shì)跟隨移動(dòng)
                if (Math.abs(mOffsetX) - Math.abs(mOffsetY) > ViewConfiguration.getTouchSlop()) {
                    int offset = (int) -mOffsetX;
                    if (getScrollX() + offset > mRightView.getWidth() || getScrollX() + offset < 0) {
                        return true;
                    }
                    this.scrollBy(offset, 0);
                    mInitX = ev.getX();
                    mInitY = ev.getY();
                    return true;
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                //松手時(shí)刻滑動(dòng)
                int offset = ((getScrollX() / (float) mRightView.getWidth()) > 0.5) ? mRightView.getWidth() : 0;
//                this.scrollTo(offset, 0);
                mScroller.startScroll(this.getScrollX(), this.getScrollY(), offset - this.getScrollX(), 0);
                invalidate();
                mInitX = 0;
                mInitY = 0;
                mOffsetX = 0;
                mOffsetY = 0;
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate(); //允許在非主線程中出發(fā)重繪,它的出現(xiàn)就是簡(jiǎn)化我們?cè)诜荱I線程更新view的步驟
        }
    }
}

主要布局

    <com.im_dsd.blogdemo.CustomScrollerView
        android:layout_width="200sp"
        android:layout_height="200sp"
        android:layout_centerInParent="true"
        android:orientation="horizontal"
        >

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/holo_blue_light"/>

        <TextView
            android:layout_width="100sp"
            android:layout_height="match_parent"
            android:background="@android:color/holo_green_light"/>
    </com.im_dsd.blogdemo.CustomScrollerView>
項(xiàng)目效果

通過(guò)上面實(shí)例我們可以發(fā)現(xiàn)在自定義View的過(guò)程中使用Scroller的流程如下圖所示:

下面我們就按照這個(gè)流程進(jìn)行源碼分析吧

2.2 源碼分析

對(duì)于Scroller類 Google給出的如下解釋:

This class encapsulates scrolling. You can use scrollers ( Scroller or OverScroller) to collect the data you need to produce a scrolling animation
for example, in response to a fling gesture. Scrollers track scroll offsets for you over time, but they don't automatically apply those positions to your view. It's your responsibility to get and apply new coordinates at a rate that will make the scrolling animation look smooth.

我們中可以看出:Scroller 是一個(gè)工具類,它只是產(chǎn)生一些坐標(biāo)數(shù)據(jù),而真正讓View平滑的滾動(dòng)起來(lái)還需要我們自行處理。我們使用的處理工具就是—— scrollTo與scrollBy

2.2.1 構(gòu)造方法分析

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

public Scroller(Context context, Interpolator interpolator) {
    this(context, interpolator,
        context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
}

public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
    mFinished = true;
    if (interpolator == null) {
        mInterpolator = new ViscousFluidInterpolator();
    } else {
        mInterpolator = interpolator;
    }
    mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
    //摩擦力計(jì)算單位時(shí)間減速度
    mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
    mFlywheel = flywheel;

    mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
}

Scroller的構(gòu)造方法沒啥特殊的地方只不過(guò)第二個(gè)參數(shù)interpolator是插值器,不同的插值器實(shí)現(xiàn)不同的動(dòng)畫算法(這里不是重點(diǎn)不做展開,以后重點(diǎn)講解),如果我們不傳,則默認(rèn)使用ViscousFluidInterpolator()插值器。

2.2.2 startScroll與fling

/** 
     * 使用默認(rèn)滑動(dòng)時(shí)間完成滑動(dòng) 
     */  
    public void startScroll(int startX, int startY, int dx, int dy) {  
        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);  
    }  
  
    /** 
     * 在我們想要滾動(dòng)的地方調(diào)運(yùn),準(zhǔn)備開始滾動(dòng),手動(dòng)設(shè)置滾動(dòng)時(shí)間
     *  
     * @param startX  滑動(dòng)起始X坐標(biāo) 
     * @param startY     滑動(dòng)起始Y坐標(biāo) 
     * @param dx    X方向滑動(dòng)距離 
     * @param dy   Y方向滑動(dòng)距離 
     * @param duration  完成滑動(dòng)所需的時(shí)間      
     */  
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {  
        mMode = SCROLL_MODE;  
        mFinished = false;  
        mDuration = duration;  
        mStartTime = AnimationUtils.currentAnimationTimeMillis();//獲取當(dāng)前時(shí)間作為滑動(dòng)的起始時(shí)間  
        mStartX = startX;  
        mStartY = startY;  
        mFinalX = startX + dx;  
        mFinalY = startY + dy;  
        mDeltaX = dx;  
        mDeltaY = dy;  
        mDurationReciprocal = 1.0f / (float) mDuration;  
    }  

    /** 
     * 開始基于滑動(dòng)手勢(shì)的滑動(dòng)。根據(jù)初始的滑動(dòng)手勢(shì)速度,決定滑動(dòng)的距離(滑動(dòng)的距離,不能大于設(shè)定的最大值,不能小于設(shè)定的最小值)  
     */
public void fling(int startX, int startY, int velocityX, int velocityY,
    int minX, int maxX, int minY, int maxY) {
    ......
    mMode = FLING_MODE;
    mFinished = false;
    ......
    mStartX = startX;
    mStartY = startY;
    ......
    mDistance = (int) (totalDistance * Math.signum(velocity));

    mMinX = minX;
    mMaxX = maxX;
    mMinY = minY;
    mMaxY = maxY;
    ......
    mFinalY = Math.min(mFinalY, mMaxY);
    mFinalY = Math.max(mFinalY, mMinY);
}

在這兩個(gè)方法中,都是一些全局變量的賦值,果真沒有實(shí)現(xiàn)滾動(dòng)的方法,也佐證了Scroller是一個(gè)工具的解讀。而要實(shí)現(xiàn)滑動(dòng)還是要依靠我們手動(dòng)調(diào)用View的invalidated()方法觸發(fā)computeScroll()方法。

   @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
             postInvalidate(); //允許在非主線程中出發(fā)重繪,它的出現(xiàn)就是簡(jiǎn)化我們?cè)诜荱I線程更新view的步驟
        }
    }

一旦觸發(fā)成功就會(huì)調(diào)用Scroller.computeScrollOffset()方法,返回結(jié)果如果為true表示當(dāng)前的滑動(dòng)尚未結(jié)束,如果返回false表示滑動(dòng)完成。
在Scroller類中,最最重要的就是這個(gè)computeScrollOffset方法,看上去只是返回了一個(gè)boolean類型,但他卻是Scroller的核心,所有的坐標(biāo)與滑動(dòng)時(shí)間都由它計(jì)算完成。他將原本瞬間的滑動(dòng)拆分成連續(xù)平滑的過(guò)程。

/** 
     * Call this when you want to know the new location.  If it returns true, 
     * the animation is not yet finished.  loc will be altered to provide the 
     * new location. 
     * 調(diào)用這個(gè)函數(shù)獲得新的位置坐標(biāo)(滑動(dòng)過(guò)程中)。如果它返回true,說(shuō)明滑動(dòng)沒有結(jié)束。 
     * getCurX(),getCurY()方法就可以獲得計(jì)算后的值。 
     */   
    public boolean computeScrollOffset() {  
        if (mFinished) {//是否結(jié)束  
            return false;  
        }  
  
        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);//滑動(dòng)開始,經(jīng)過(guò)了多長(zhǎng)時(shí)間  
      
        if (timePassed < mDuration) {//如果經(jīng)過(guò)的時(shí)間小于動(dòng)畫完成所需時(shí)間  
            switch (mMode) {  
            case SCROLL_MODE:  
                float x = timePassed * mDurationReciprocal;  
      
                if (mInterpolator == null)//如果沒有設(shè)置插值器,利用默認(rèn)算法  
                    x = viscousFluid(x);   
                else//否則利用插值器定義的算法  
                    x = mInterpolator.getInterpolation(x);  
      
                mCurrX = mStartX + Math.round(x * mDeltaX);//計(jì)算當(dāng)前X坐標(biāo)  
                mCurrY = mStartY + Math.round(x * mDeltaY);//計(jì)算當(dāng)前Y坐標(biāo)  
                break;  
            case FLING_MODE:  
                final float t = (float) timePassed / mDuration;  
                final int index = (int) (NB_SAMPLES * t);  
                final float t_inf = (float) index / NB_SAMPLES;  
                final float t_sup = (float) (index + 1) / NB_SAMPLES;  
                final float d_inf = SPLINE[index];  
                final float d_sup = SPLINE[index + 1];  
                final float distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf);  
                  
                mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));  
                // Pin to mMinX <= mCurrX <= mMaxX  
                mCurrX = Math.min(mCurrX, mMaxX);  
                mCurrX = Math.max(mCurrX, mMinX);  
                  
                mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));  
                // Pin to mMinY <= mCurrY <= mMaxY  
                mCurrY = Math.min(mCurrY, mMaxY);  
                mCurrY = Math.max(mCurrY, mMinY);  
  
                if (mCurrX == mFinalX && mCurrY == mFinalY) {  
                    mFinished = true;  
                }  
  
                break;  
            }  
        }  
        else {  
            mCurrX = mFinalX;  
            mCurrY = mFinalY;  
            mFinished = true;  
        }  
        return true;  
    }  

從代碼可以看到,如果我們沒有設(shè)置插值器,就會(huì)調(diào)用內(nèi)部默認(rèn)算法。

/** 
     * 函數(shù)翻譯是粘性流體 
     * 估計(jì)是一種算法 
     */  
    static float viscousFluid(float x)  
    {  
        x *= sViscousFluidScale;  
        if (x < 1.0f) {  
            x -= (1.0f - (float)Math.exp(-x));  
        } else {  
            float start = 0.36787944117f;   // 1/e == exp(-1)  
            x = 1.0f - (float)Math.exp(1.0f - x);  
            x = start + x * (1.0f - start);  
        }  
        x *= sViscousFluidNormalize;  
        return x;  
    }  

接著是兩個(gè)重要的get方法

/** 
     * Returns the current X offset in the scroll.  
     *  
     * @return The new X offset as an absolute distance from the origin. 
     * 獲得當(dāng)前X方向偏移 
     */  
    public final int getCurrX() {  
        return mCurrX;  
    }  
      
    /** 
     * Returns the current Y offset in the scroll.  
     *  
     * @return The new Y offset as an absolute distance from the origin. 
     * 獲得當(dāng)前Y方向偏移 
     */  
    public final int getCurrY() {  
        return mCurrY;  
    }  

2.2.3 其他方法

public class Scroller  {
    ......
    public Scroller(Context context) {}
    public Scroller(Context context, Interpolator interpolator) {}
    public Scroller(Context context, Interpolator interpolator, boolean flywheel) {}
    //設(shè)置滾動(dòng)持續(xù)時(shí)間
    public final void setFriction(float friction) {}
    //返回滾動(dòng)是否結(jié)束
    public final boolean isFinished() {}
    //強(qiáng)制終止?jié)L動(dòng)
    public final void forceFinished(boolean finished) {}
        //返回滾動(dòng)持續(xù)時(shí)間
    public final int getDuration() {}
    //返回當(dāng)前滾動(dòng)的偏移量
    public final int getCurrX() {}
    public final int getCurrY() {}
    //返回當(dāng)前的速度
    public float getCurrVelocity() {}
    //返回滾動(dòng)起始點(diǎn)偏移量
    public final int getStartX() {}
    public final int getStartY() {}
        //返回滾動(dòng)結(jié)束偏移量
    public final int getFinalX() {}
    public final int getFinalY() {}
    //實(shí)時(shí)調(diào)用該方法獲取坐標(biāo)及判斷滑動(dòng)是否結(jié)束,返回true動(dòng)畫沒結(jié)束
    public boolean computeScrollOffset() {}
    //滑動(dòng)到指定位置
    public void startScroll(int startX, int startY, int dx, int dy) {}
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {}
    //快速滑動(dòng)松開手勢(shì)慣性滑動(dòng)
    public void fling(int startX, int startY, int velocityX, int velocityY,
            int minX, int maxX, int minY, int maxY) {}
    //終止動(dòng)畫,滾到最終的x、y位置
    public void abortAnimation() {}
    //延長(zhǎng)滾動(dòng)的時(shí)間
    public void extendDuration(int extend) {}
    //返回滾動(dòng)開始經(jīng)過(guò)的時(shí)間
    public int timePassed() {}
    //設(shè)置終止時(shí)偏移量
    public void setFinalX(int newX) {}
    public void setFinalY(int newY) {}
}

3 總結(jié):

  1. 滑動(dòng)的本質(zhì)就是View隨著手指的運(yùn)動(dòng)不斷地改變坐標(biāo)
  2. scrollTo(x,y)指的就是View滾動(dòng)到(x,y)這個(gè)位置,但是View 要相當(dāng)于父控件靜止不懂,所以相對(duì)的View的內(nèi)容就會(huì)滑動(dòng)到(-x, -y)的位置
  3. scrollTo、scrollBy移動(dòng)是瞬間的
  4. 滑動(dòng)效果作用的對(duì)象是View內(nèi)容
  5. Scroller類其實(shí)是一個(gè)工具類,生產(chǎn)滑動(dòng)過(guò)程的平滑坐標(biāo),但最終的滑動(dòng)動(dòng)作還是需要我們自行處理
  6. Scroller類的使用流程:

參考

《Android群英傳》
http://blog.csdn.net/crazy__chen/article/details/45896961
http://blog.csdn.net/yanbober/article/details/49904715

版權(quán)聲明:
禁止一切商業(yè)行為,轉(zhuǎn)載請(qǐng)著名出處 http://blog.csdn.net/qq_23191031。作者: 大圣代
Copyright (c) 2017 代圣達(dá). All rights reserved.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,323評(píng)論 25 708
  • 什么是View View 是 Android 中所有控件的基類。 View的位置參數(shù) View 的位置由它的四個(gè)頂...
    acc8226閱讀 1,409評(píng)論 0 7
  • 導(dǎo)語(yǔ) 滑動(dòng)算是Android比較常用的效果了,滑動(dòng)的操作具有很好的用戶體驗(yàn)性。 主要內(nèi)容 滑動(dòng)效果是如何產(chǎn)生的 實(shí)...
    一個(gè)有故事的程序員閱讀 6,565評(píng)論 3 11
  • [轉(zhuǎn)載]被“辯證法”毒害的中國(guó)人 一個(gè)無(wú)敵句式——你要辯證的看問(wèn)題 無(wú)論你說(shuō)啥觀點(diǎn),“辯證的看問(wèn)題”都能將你輕易擊...
    白鹿格桑閱讀 2,537評(píng)論 0 6
  • 第一天彩鉛畫櫻桃 給跪了,完全不是那么回事啊,覺比之前用中性筆畫畫難萬(wàn)倍。一種畫叫人家的,一種畫叫我畫的。 看來(lái)我...
    吉祥天的夏天閱讀 529評(píng)論 3 4

友情鏈接更多精彩內(nèi)容