【自定义View系列】View的事件分发机制

1026人阅读 评论(0) 收藏 举报
分类:

  本部分介绍View的一个核心知识点:事件分发机制。事件分发机制不仅仅是核心知识点更是难点,不少初学者甚至中级开发者面对这个问题都会觉得困惑。另外,View的另一大难题滑动冲突,它的解决方法的理论基础就是事件分发机制,因此掌握好View的事件分发机制是十分重要的。

一.为什么需要事件分发机制

http://blog.csdn.net/aigestudio/article/details/44260301
http://blog.csdn.net/aigestudio/article/details/44746625

  aige已经写过两篇相关的博客,非常清晰的解释了为何有事件分发机制,这里就不再赘述了。其实,不止android,一切有界面的系统都有自己的事件分发机制,如windows系统、osx系统,ios系统等等。

二.事件分发机制的类比案例

  假设点击事件是一个难题,经理把这个难题分给组长去处理,组长又分给程序员处理,程序员解决不了,只能交给组长,组长来解决,组长解决不了,那只能继续往上交给经理,经理来解决。从这个角度看,事件分发机制还是很贴近现实的。

三.MotionEvent

常量:
public static final int ACTION_DOWN = 0;单点触摸按下动作
public static final int ACTION_UP = 1;单点触摸离开动作
public static final int ACTION_MOVE = 2;触摸点移动动作
public static final int ACTION_CANCEL = 3;触摸动作取消
ACTION_MASK = 0X000000ff 动作掩码 为了得到action

方法:
getAction() 返回值:int 使用掩码后可获得上面的对应常量值
getX(),getY() 相对当前view的坐标
getRawX(),getRawY() 相对屏幕的坐标

四.依照惯例,我们仍然从栈帧分析入手

  栈帧清晰的帮我们列出了方法的调用流程,是阅读源码非常主要的工具。

1.栈帧

这里写图片描述

布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.ht.androidstudy.view.DebugTextView
        android:id="@+id/debugView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="dddd"
        />


</LinearLayout>

DebugTextView:

public class DebugTextView extends TextView {

    public DebugTextView(Context context) {
        super(context);
    }

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


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Log.d("dd", "");
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }


    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        Log.d("dd", "");
        super.onLayout(changed, left, top, right, bottom);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        Log.d("dd", "");
        super.onDraw(canvas);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("dd", "");
        return super.onTouchEvent(event);
    }
}

  onTouchEvent方法的 Log.d(“dd”, “”);加上断点debug后的栈帧如上图,我们抛开系统对UI的影响,只分析LinearLayout和DebugTextView。

2.Activity对点击事件的分发过程

  点击事件用MotionEvent来表示,当一个点击操作发生时,事件最先传递给当前Activity,由Activity的dispatchtouchevent来进行事件派发,具体的工作是由Activity内部的Window完成的。Window会将事件传递给DecorView,DecorView一般就是当前界面的底层容器,即SetContentView所设置的View的父容器),通过Activity.getWindow().getDecorView()可以获得。我们从Activity的dispatchtouchevent开始分析。源码如下:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        // 主要是走的这个分支,getWindow就是PhoneWindow的一个对象
        // PhoneWindow里的superDispatchTouchEvent走的是
        // mDecor.superDispatchTouchEvent(event);
        // 所以最终走到了mDecor的superDispatchTouchEvent方法
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

mDecor的superDispatchTouchEvent源码如下:

        public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }

因为mDecor继承自FrameLayout,所以走的是FrameLayout的dispatchTouchEvent方法。

3.顶级View(即LinearLayout)对点击事件的分发过程

我们跳过mDecor的分发看LinearLayout
LinearLayout的dispatchTouchEvent源码如下:

   @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        final float xf = ev.getX();
        final float yf = ev.getY();
        final float scrolledXFloat = xf + mScrollX;
        final float scrolledYFloat = yf + mScrollY;
        final Rect frame = mTempRect;

        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

        if (action == MotionEvent.ACTION_DOWN) {
            if (mMotionTarget != null) {
                // this is weird, we got a pen down, but we thought it was
                // already down!
                // XXX: We should probably send an ACTION_UP to the current
                // target.
                mMotionTarget = null;
            }
            // If we're disallowing intercept or if we're allowing and we didn't
            // intercept
            // 首先走onInterceptTouchEvent方法,判断是否拦截
            // 不拦截的话,就会进入if里面,做事件分发,主要是调用子View的dispatchTouchEvent
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {
                // reset this event's action (just to protect ourselves)
                ev.setAction(MotionEvent.ACTION_DOWN);
                // We know we want to dispatch the event down, find a child
                // who can handle it, start with the front-most child.
                final int scrolledXInt = (int) scrolledXFloat;
                final int scrolledYInt = (int) scrolledYFloat;
                final View[] children = mChildren;
                final int count = mChildrenCount;
                for (int i = count - 1; i >= 0; i--) {
                    final View child = children[i];
                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                            || child.getAnimation() != null) {
                        child.getHitRect(frame);
                        // 点击事件的坐标是否落在子元素的区域内
                        if (frame.contains(scrolledXInt, scrolledYInt)) {
                            // offset the event to the view's coordinate system
                            final float xc = scrolledXFloat - child.mLeft;
                            final float yc = scrolledYFloat - child.mTop;
                            ev.setLocation(xc, yc);
                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                            if (child.dispatchTouchEvent(ev))  {
                                // Event handled, we have a target now.
                                // mMotionTarget什么情况下会被赋值,并终止这个循环?
                                // 如果子元素的dispatchTouchEvent方法返回true
                                // 那么mMotionTarget就会被赋值,并跳出for循环
                                mMotionTarget = child;
                                return true;
                            }
                            // The event didn't get handled, try the next view.
                            // Don't reset the event's location, it's not
                            // necessary here.
                        }
                    }
                }
            }
        }

        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
                (action == MotionEvent.ACTION_CANCEL);

        if (isUpOrCancel) {
            // Note, we've already copied the previous state to our local
            // variable, so this takes effect on the next event
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // The event wasn't an ACTION_DOWN, dispatch it to our target if
        // we have one.
        final View target = mMotionTarget;

        // 如果遍历所有的子元素后事件都没有被合适的处理,这包含了两种情况:
        // 1. ViewGroup没有子元素
        // 2. 子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这一般是
        // 因为子元素在onTouchEvent中返回了false。
        // 这两种情况下就会走View的dispatchTouchEvent方法,然后自己来处理
        if (target == null) {
            // We don't have a target, this means we're handling the
            // event as a regular view.
            ev.setLocation(xf, yf);
            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
                ev.setAction(MotionEvent.ACTION_CANCEL);
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            }
            return super.dispatchTouchEvent(ev);
        }

        // if have a target, see if we're allowed to and want to intercept its
        // events
        if (!disallowIntercept && onInterceptTouchEvent(ev)) {
            final float xc = scrolledXFloat - (float) target.mLeft;
            final float yc = scrolledYFloat - (float) target.mTop;
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            ev.setAction(MotionEvent.ACTION_CANCEL);
            ev.setLocation(xc, yc);
            if (!target.dispatchTouchEvent(ev)) {
                // target didn't handle ACTION_CANCEL. not much we can do
                // but they should have.
            }
            // clear the target
            mMotionTarget = null;
            // Don't dispatch this event to our own view, because we already
            // saw it when intercepting; we just want to give the following
            // event to the normal onTouchEvent().
            return true;
        }

        if (isUpOrCancel) {
            mMotionTarget = null;
        }

        // finally offset the event to the target's coordinate system and
        // dispatch the event.
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setLocation(xc, yc);

        if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            ev.setAction(MotionEvent.ACTION_CANCEL);
            target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            mMotionTarget = null;
        }

        return target.dispatchTouchEvent(ev);
    }

具体的解释看代码注释即可。

3.LinearLayout分发就到了DebugTextView的dispatchTouchEvent方法

走的是View的dispatchTouchEvent方法

    public boolean dispatchTouchEvent(MotionEvent event) {
        // View对点击事件的处理过程就比较简单了,因为View是一个单独的元素,他没有子元素
        // 因此无法向下传递事件,所以只能自己处理事件。
        // 从下面可见,View对点击事件的处理过程,它会首先判断有没有设置onTouchListener,
        // 如果onTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见
        // onTouchListener中的onTouch方法优先级高于onTouchEvent。
        // 这样做的好处是方便我们在外界处理点击事件,就像自定义view总会留出setxxx接口一样
        // 其实这里是同样的道理
        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                mOnTouchListener.onTouch(this, event)) {
            return true;
        }
        return onTouchEvent(event);
    }

4.继续看DebugTextView onTouchEvent方法

 public boolean onTouchEvent(MotionEvent event) {
        final int viewFlags = mViewFlags;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
        }

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }


        // 从这里可以看出,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗
        // 这个事件,即onTouchEvent返回true
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            // 这里是ontouchevent中点击事件的具体处理
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
                    if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (!mHasPerformedLongPress) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    // 当ACTION_UP事件发生时,会触发performClick方法,如果
                                    // View设置了onClickLinstener,那么performClick方法内部会调用
                                    // 它的onClick方法
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            mPrivateFlags |= PRESSED;
                            refreshDrawableState();
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                        removeTapCallback();
                    }
                    break;

                case MotionEvent.ACTION_DOWN:
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPrivateFlags |= PREPRESSED;
                    mHasPerformedLongPress = false;
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    break;

                case MotionEvent.ACTION_CANCEL:
                    mPrivateFlags &= ~PRESSED;
                    refreshDrawableState();
                    removeTapCallback();
                    break;

                case MotionEvent.ACTION_MOVE:
                    final int x = (int) event.getX();
                    final int y = (int) event.getY();

                    // Be lenient about moving outside of buttons
                    int slop = mTouchSlop;
                    if ((x < 0 - slop) || (x >= getWidth() + slop) ||
                            (y < 0 - slop) || (y >= getHeight() + slop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            // Need to switch from pressed to not pressed
                            mPrivateFlags &= ~PRESSED;
                            refreshDrawableState();
                        }
                    }
                    break;
            }
            return true;
        }

        return false;
    }

至此,对这个栈帧的分析已经结束,下面会做提取结论,做以总结。

五.总结

1.隧道式向下分发,然后冒泡式向上处理。

  当一个点击事件发生的时候,它的传递过程遵循如下顺序:Activity-Window-ViewGroup-View。
  处理按照是否消费的返回值,从下到上返回,即如果View的onTouchEvent返回false,将会向上传给它的parent的ViewGroup,如果ViewGroup的onTouchEvent也返回false,,将会一直向上返回到Activity,即activity的onTouchEvent方法会被调用。
  activity和window的拦截方法我们一般不修改,因为不具备应用价值。

2.ViewGRoup的事件传递机制

1)dispatchTouchEvent

 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        final float xf = ev.getX();
        final float yf = ev.getY();
        final float scrolledXFloat = xf + mScrollX;
        final float scrolledYFloat = yf + mScrollY;
        final Rect frame = mTempRect;

        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

        if (action == MotionEvent.ACTION_DOWN) {
            if (mMotionTarget != null) {
                // this is weird, we got a pen down, but we thought it was
                // already down!
                // XXX: We should probably send an ACTION_UP to the current
                // target.
                mMotionTarget = null;
            }
            // If we're disallowing intercept or if we're allowing and we didn't
            // intercept
            // 首先走onInterceptTouchEvent方法,判断是否拦截
            // 不拦截的话,就会进入if里面,做事件分发,主要是调用子View的dispatchTouchEvent
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {
                // reset this event's action (just to protect ourselves)
                ev.setAction(MotionEvent.ACTION_DOWN);
                // We know we want to dispatch the event down, find a child
                // who can handle it, start with the front-most child.
                final int scrolledXInt = (int) scrolledXFloat;
                final int scrolledYInt = (int) scrolledYFloat;
                final View[] children = mChildren;
                final int count = mChildrenCount;
                for (int i = count - 1; i >= 0; i--) {
                    final View child = children[i];
                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                            || child.getAnimation() != null) {
                        child.getHitRect(frame);
                        // 点击事件的坐标是否落在子元素的区域内
                        if (frame.contains(scrolledXInt, scrolledYInt)) {
                            // offset the event to the view's coordinate system
                            final float xc = scrolledXFloat - child.mLeft;
                            final float yc = scrolledYFloat - child.mTop;
                            ev.setLocation(xc, yc);
                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                            if (child.dispatchTouchEvent(ev))  {
                                // Event handled, we have a target now.
                                // mMotionTarget什么情况下会被赋值,并终止这个循环?
                                // 如果子元素的dispatchTouchEvent方法返回true
                                // 那么mMotionTarget就会被赋值,并跳出for循环
                                mMotionTarget = child;
                                return true;
                            }
                            // The event didn't get handled, try the next view.
                            // Don't reset the event's location, it's not
                            // necessary here.
                        }
                    }
                }
            }
        }

        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
                (action == MotionEvent.ACTION_CANCEL);

        if (isUpOrCancel) {
            // Note, we've already copied the previous state to our local
            // variable, so this takes effect on the next event
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // The event wasn't an ACTION_DOWN, dispatch it to our target if
        // we have one.
        final View target = mMotionTarget;

        // 如果遍历所有的子元素后事件都没有被合适的处理,这包含了两种情况:
        // 1. ViewGroup没有子元素
        // 2. 子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这一般是
        // 因为子元素在onTouchEvent中返回了false。
        // 这两种情况下就会走View的dispatchTouchEvent方法,然后自己来处理
        if (target == null) {
            // We don't have a target, this means we're handling the
            // event as a regular view.
            ev.setLocation(xf, yf);
            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
                ev.setAction(MotionEvent.ACTION_CANCEL);
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            }
            return super.dispatchTouchEvent(ev);
        }

        // if have a target, see if we're allowed to and want to intercept its
        // events
        if (!disallowIntercept && onInterceptTouchEvent(ev)) {
            final float xc = scrolledXFloat - (float) target.mLeft;
            final float yc = scrolledYFloat - (float) target.mTop;
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            ev.setAction(MotionEvent.ACTION_CANCEL);
            ev.setLocation(xc, yc);
            if (!target.dispatchTouchEvent(ev)) {
                // target didn't handle ACTION_CANCEL. not much we can do
                // but they should have.
            }
            // clear the target
            mMotionTarget = null;
            // Don't dispatch this event to our own view, because we already
            // saw it when intercepting; we just want to give the following
            // event to the normal onTouchEvent().
            return true;
        }

        if (isUpOrCancel) {
            mMotionTarget = null;
        }

        // finally offset the event to the target's coordinate system and
        // dispatch the event.
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setLocation(xc, yc);

        if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            ev.setAction(MotionEvent.ACTION_CANCEL);
            target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            mMotionTarget = null;
        }

        return target.dispatchTouchEvent(ev);
    }

    /**
     * {@inheritDoc}
     */
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

2)onInterceptTouchEvent:是否拦截

如果父布局在拦截方法中重写了down\move\up这些事件的拦截,那么每次都会走down\move\up的拦截,然后才会走ontouchevent。

3)onTouchEvent

采用的是View的onEvent方法。长按事件的响应,点击事件的响应都在这个方法中。

3.View的事件传递机制

1)dispatchTouchEvent

    public boolean dispatchTouchEvent(MotionEvent event) {
        // View对点击事件的处理过程就比较简单了,因为View是一个单独的元素,他没有子元素
        // 因此无法向下传递事件,所以只能自己处理事件。
        // 从下面可见,View对点击事件的处理过程,它会首先判断有没有设置onTouchListener,
        // 如果onTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见
        // onTouchListener中的onTouch方法优先级高于onTouchEvent。
        // 这样做的好处是方便我们在外界处理点击事件,就像自定义view总会留出setxxx接口一样
        // 其实这里是同样的道理
        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                mOnTouchListener.onTouch(this, event)) {
            return true;
        }
        return onTouchEvent(event);
    }

可以看到dispatchTouchEvent方法的返回值是受onInterceptTouchEvent和onTouchEvent影响的,并不是彼此不相干的。

2)无这个方法onInterceptTouchEvent

3)onTouchEvent,上面已经分析过

长按事件的响应,点击事件的响应都在这个方法中。

4.处理滑动冲突

1)外部拦截法

重写ViewGroup的onInterceptTouchEvent方法.
在onDown不拦截(否则无法再向下传递),需要拦截其他事件时返回true
伪代码:

public boolean onInterceptTouchEvent(MotionEvent ev){
boolean intercepted=false;
switch(ev.getAction()){
case MotionEvent.ACTION_DOWN:
intercepted=false;
break;
case MotionEvent.ACTION_MOVE:
if(xxx) intercepted=true;  //拦截
else intercepted=false; //不拦截
break;
case MotionEvent.ACTION_UP:
intercepted=false;
break;
}
return intercepted;
}

2)内部拦截法.

在view中控制viewGroup来实现,view通过改变FLAG_DISALLOW_INTERCEPT的状态来控制ViewGroup是否拦截后续事件
伪代码:

public boolean dispatchTouchEvent(MotionEvent event){
    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            if(需要上级拦截){
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        default:
            break;
    }
    return super.dispatchTouchEvent(event);
}

5.涉及类和方法的类图结构

6.应用场合

  • 自定义view
    一般也是改变返回值,具体的代码实现几乎不会改动

  • 滑动冲突(事件分发机制是它的理论基础)
    产生场合
    如何解决

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    写给自己
    ○ 种一棵树最好的时间是十年前,其次是现在

    ○ 坚持输出,坚持书写,才可以持续成长

    ○ 所有美好事物的成长都是缓慢的

    ○ 既往不恋,未来不迎,当下不杂

    ○ 业精于勤,荒于嬉,行成于思,毁于随

    ○将军赶路 不追小兔

    ○不要拘泥于语言,同样也不要拘泥于行业,眼光放远一点

    ○ 如果某件事你做的不够好,不必介怀,因为以后的每一次每一天你都会做得越来越好

    ○ 此心不于事上磨,更于何处磨此心

    ○ 保持热情,保持求知欲

    ○ 千里之行,始于足下

    ○ 最怕你一生碌碌无为,还安慰自己平凡可贵。

    ○ 对于任何事,要保持自觉积极主动探索尝试。但是如果自己不积极认真地生活,不管得到什么样的回答都没有用。——解忧杂货店
    个人资料
    • 访问:649859次
    • 积分:94
    • 等级:
    • 排名:千里之外
    • 原创:291篇
    • 转载:50篇
    • 译文:0篇
    • 评论:114条
    个人简介