博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
View的事件体系
阅读量:5755 次
发布时间:2019-06-18

本文共 21630 字,大约阅读时间需要 72 分钟。

一. View基础知识

  • view的位置参数
  • MotionEvent和TouchSlop
  • Velocity,GentureDetecor和Scroller

二. 什么是View

View是Android中所有的组件的基类,包括系统提供的控件,如:Button,TextView,Relativelayout和Listview还是自定义控件他们的共同基类都是view,所以说.View是一种界面层的控件的一种抽象,它代表了一个控件.除了View,还有ViewGroup,从名字来看,它可以翻译为控件组,ViewGroup里面可以包括多个控件,即一组View,在Android的设计中,ViewGroup也继承View,View本身就可以是单个控件也可以是多个控件组成的一组控件,这种关系就View树的结构.

例如:Button显然是一个View,但LinearLayout不但是一个View而且还是一个ViewGroup,而ViewGroup内部可以有子View的,这个子View可能还是View复制代码

三. View的位置参数

  • top 左上角纵坐标
  • left 左上角横坐标
  • right 右下角横坐标
  • bottom 右下角纵坐标 这些坐标都是以父容器为参考系的,因此它是一种相对坐标
width = right - leftheight = bottom - top复制代码

那么如何得到这四个参数呢?

Left = getLeft()Right =getRight()Top = getTop()Bottom = getBottom()复制代码

从 Android 3.0 开始增加了这几个额外的参数 x,y.translationX 和 teanslationY , 其中 x, y 是 View 左上角的坐标.而 translationX 和 teanslationY 是 View 左上角相对于的偏移量. 这几个参数也是相对于父容器的偏移量. translationX 和 teanslationY 默认值是 0 , View 也为 他们提供默认的set/get 方法

x = left + translationXy = top + teanslationY复制代码

四. MotionEvent和TouchSlop

①.MotionEvent

  • 手指触摸屏幕后产生一系列事件
  • ACTION_DOWN -- 手指刚接触屏幕
  • ACTION_MOVE -- 手指在屏幕上移动
  • ACTION_UP -- 手指从屏幕松开的一瞬间
  • 正常情况,一次手指触摸屏幕的行为会触发一系列的点击事件
  • 点击屏幕后离开松开 事件序列 DOWN --> UP
  • 点击屏幕滑动一会再松开 DOWN --> MOVE --> .. --> MOVE --> UP
  • 通过 MotionEvent 对象我们可以获取 点击事件发生的 x 和 y 坐标,为此系统提供了 两组方法:
  • getX / getY (当前View 左上角 x 坐标 和 y坐标)
  • getRawX / getRawY (屏幕左上角 x 坐标 和 y 坐标)

②.TouchSlop

TouchSlop 是系统所能识别出被认为是滑动的最小距离 ,当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统不认为他是滑动的,这个常量值和设备有关,不同的设备这个值可能有所差异 如何回去这个常量呢? ViewConfiguration.get(getContext()).getScaledTouchTop()

五. Velocity,GentureDetecor和Scroller

①.Velocity

速度追踪,用来追踪手指滑动过程中的速度,包括水平速度和垂直速度,他的使用过程很简单

  • 在View 的 onTouchEvent方法中追踪当前点击事件的速度
final VelocityTracker obtain = VelocityTracker.obtain();obtain.addMovement(event);复制代码
  • 获取当前滑动速度
obtain.computeCurrentVelocity(1000);  final int xVelocity = (int) obtain.getXVelocity();  final int yVelocity = (int) obtain.getYVelocity();复制代码

当不需要的时候,注意重置回收

obtain.recycle();   obtain.clear();复制代码

②.GentureDetecor

手势检测,用户辅助检测用户单击,滑动,长按,双击行为

  • 创建一个GentureDetecor对象并实现OnGestureListener接口,根据需要我们还可以实现 OnDoubleTapListener 从而能够监听双击行为
final GestureDetector detector = new GestureDetector(this);  // 解决长按屏幕无法拖动的现象  detector.setIsLongpressEnabled(false);复制代码
  • 接管目标 View 的 onTouchEvent 方法,在待监听 View 的 onTouchEvent 方法中添加如下实现
final boolean consume = detector.onTouchEvent(event);   return consume;复制代码
方法名 描述 所属接口
onDown(触摸放开) 手指轻轻触摸屏幕一瞬间,由 1 个 ACTION_DOWN 触发 OnGestureListener
onShowPress(触摸未松动) 手指轻轻触摸屏幕,尚未松动或拖动,由1个 ACTION_DOWN 触发 * 注意 和 onDown() 区别是,强调的是没有松开或拖动的状态 OnGestureListener
onSingleTapUp(单击) 手指松开,伴随着1个 MotionEvent ACTION_UP 而触发,这是单击行为 OnGestureListener
onLongPress (长按) 用户长久地按着屏幕不放 OnGestureListener
onFling(快速滑动) 用户按下触摸屏,快速滑动松开,由1个 ACTION_DOWN ,多个 ACTION_MOVE 和 ACTION_UP触发,这就是快速滑动行为 OnGestureListener
OnDoubleTab(双击) 双击,由两次单击组成,它不可能和 OnSingleTabConfirm 共存 OnDoubleTabListener
OnSingleTabConfirm(严格单击行为) 严格单击行为,只响应一次 OnDoubleTabListener
OnDoubleTabEvent (双击) 双击行为 OnDoubleTabListener
onScroll(拖动) 手指按下屏幕并拖动,由1个ACTION_DOWN,多个ACTION_MOVE触发,这就是拖动行为 OnGestureListener

③.Scroller

弹性滑动对象

final Scroller scroller = new Scroller(this);复制代码

六. View的滑动

如何实现View的滑动

①. scrollTo / scrollBy

public void scrollTo(int x, int y) {        if (mScrollX != x || mScrollY != y) {            int oldX = mScrollX;            int oldY = mScrollY;            mScrollX = x;            mScrollY = y;            invalidateParentCaches();            onScrollChanged(mScrollX, mScrollY, oldX, oldY);            if (!awakenScrollBars()) {                postInvalidateOnAnimation();            }        }    }     public void scrollBy(int x, int y) {        scrollTo(mScrollX + x, mScrollY + y);    }复制代码
  • View 施加平滑效果实现View的滑动

  • 改变View的LayoutParam 使得 View重新布局, 从而实现滑动

②.使用动画

复制代码

为了兼容3.0以下版本我们需要引入 nineoldAndroid,但是注意一点的是View动画只能改变View的影像,并不能改变View的布局参数

③.改变布局参数

ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mMore.getLayoutParams();		params.width += 100;		params.leftMargin += 100;		mMore.requestLayout();		//或者 mMore.setLayoutParams(params);复制代码

③.各种滑动方式对比

  • scrollTo / scrollBy

适合对View内容的滑动

  • 动画

适用于没有交互的 View 和 实现复杂的动画效果

  • 改变布局

操作复杂,适用于交互的View

@Override    public boolean onTouchEvent(MotionEvent event) {        final int x = (int) event.getRawX();        final int y = (int) event.getRawY();        switch (event.getAction()){            case MotionEvent.ACTION_DOWN:                break;            case MotionEvent.ACTION_MOVE:                int deltaX =	x - mLastX;                int deltaY =	y - mLastY;                final int translationX = (int)ViewHelper.getTranslationX(this) + deltaX;                final int translationY = (int)ViewHelper.getTranslationX(this) + deltaY;                ViewHelper.setTranslationX(this,translationX);                ViewHelper.setTranslationY(this,translationY);                break;            case MotionEvent.ACTION_UP:                break;            default:                break;                   }        mLastX = x;        mLastY = y;        return true;    }复制代码

④.弹性滑动

如何实现弹性滑动?

将一次大的滑动分成若干的小滑动,并在一定的时间内完成

⑤.使用Scroller

public void startScroll(int startX, int startY, int dx, int dy, int duration) {        mMode = SCROLL_MODE;        mFinished = false;        mDuration = duration;        mStartTime = AnimationUtils.currentAnimationTimeMillis();        mStartX = startX;        mStartY = startY;        mFinalX = startX + dx;        mFinalY = startY + dy;        mDeltaX = dx;        mDeltaY = dy;        mDurationReciprocal = 1.0f / (float) mDuration;    }复制代码
  • Scroller 如何让 View 滑动的?
public boolean computeScrollOffset() {        if (mFinished) {            return false;        }        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);            if (timePassed < mDuration) {            switch (mMode) {            case SCROLL_MODE:                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);                mCurrX = mStartX + Math.round(x * mDeltaX);                mCurrY = mStartY + Math.round(x * mDeltaY);                break;            case FLING_MODE:                final float t = (float) timePassed / mDuration;                final int index = (int) (NB_SAMPLES * t);                float distanceCoef = 1.f;                float velocityCoef = 0.f;                if (index < NB_SAMPLES) {                    final float t_inf = (float) index / NB_SAMPLES;                    final float t_sup = (float) (index + 1) / NB_SAMPLES;                    final float d_inf = SPLINE_POSITION[index];                    final float d_sup = SPLINE_POSITION[index + 1];                    velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);                    distanceCoef = d_inf + (t - t_inf) * velocityCoef;                }                mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;                                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;    }复制代码

⑥.通过动画

TODO: 博主暂时也没有弄明白

ObjectAnimator.ofFloat(view, "translationX", 0, 100).setDuration(100).start();		final int startX = 0;		final int startY = 100;        final ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000);        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {            @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)            @Override            public void onAnimationUpdate(ValueAnimator animation) {                final float fraction = animator.getAnimatedFraction();                mHome.scrollTo(startX + (deltaX * fraction),0);            }        });        animator.start();复制代码

⑦.使用延时策略

通过发送一系列的延时消息从而达到一种渐进式的效果

private static final int MESSAGE_SCROLL_TO = 1;    private static final int FRAME_COUNT = 30;    private static final int DELAYED_TIME = 33;    private int mCount = 0;    protected Handler mHandler = new Handler() {        @Override        public void handleMessage(Message msg) {            super.handleMessage(msg);            int what = msg.what;            switch (what) {                case MESSAGE_SCROLL_TO:                    mCount++;                    if (mCount <= FRAME_COUNT) {                        final float fraction = mCount / (float) FRAME_COUNT;                        final int scrollX = (int) (fraction * 100);                        mAutoLogin.scrollTo(scrollX,0);                        mHandler.sendMessageDelayed(MESSAGE_SCROLL_TO,DELAYED_TIME);                    }                    break;}}复制代码

七. View的事件分发机制

①. 点击事件的传递规则

点击事件传递过程中涉及一个很重要的API,就是MotionEvent.所谓的点击事件的事件分发就是对MotionEvent事件的分发过程,当一个MotionEvent产生后,系统需要将这个事件传递给具体的View,而这个传递的过程就是分发的过程. 中间风阀过程涉及三个重点的方法:

@Override    public boolean dispatchTouchEvent(MotionEvent ev) {        return super.dispatchTouchEvent(ev);    }复制代码

进行事件分发,如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的TouchEvent和下级View的dispatchTouchEvent影响,表示正在消耗当前事件

public boolean onIntercrptTouchEvent(MotionEvent ev) {}复制代码

用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一事件序列中,此方法不会被调用

@Override    public boolean onTouchEvent(MotionEvent event) {        boolean consume = false;        if (onInterceptTouchEvent(event)){            consume = onTouchEvent(event);        }else {            consume = child.dispatchTouchEvent(event);        }        return consume;    }复制代码

对于一个根View而言,当点击事件发生以后,它的dispatchevent就会被调用,如果onInterceptTouchEvent方法返回true,就表示拦截此事件,接着事件就会交给ViewGroup处理,即他的TouchEvent会被调用,如果这个ViewGrop返回false,那么表示不拦截这个事件,这时,当前事件就会传递给他的子元素,接着子元素的dispatchEvent方法就会被调用,如此反复直至事件被消耗完毕 当一个View处理事件时,它设置了onTouchListener,那么onTouchListener中的onTouch方法就会被调用,这时事件如何处理还要看onTouch的返回值,如果返回false,则当前view的ontouchEvent方法会被调用,如果返回为true,那么onTouchEvent方法将不会被调用.由此可见onTouchListener的优先级比 onTouchEvent还高,在 onTouchEvent 方法中 , 如果当前设置的有onClickListener,那么 onClick 方法会被调用,平时我们常用的 onClickListerner 优先级最低 点击事件的传递顺序在ui层的优先顺序表现为 Activity -> Window -> View

  1. ViewGroup 默认不拦截任何事件, Android源码中 ViewGrop 默认返回 false
public boolean onInterceptTouchEvent(MotionEvent ev) {        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)                && ev.getAction() == MotionEvent.ACTION_DOWN                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)                && isOnScrollbarThumb(ev.getX(), ev.getY())) {            return true;        }        return false;    }复制代码
  1. View 没有 oninterceptEvent 方法,一旦点击事件传递给它,那么它的 onTouchEvent 就会被调用
public boolean onTouchEvent(MotionEvent event) {        final float x = event.getX();        final float y = event.getY();        final int viewFlags = mViewFlags;        final int action = event.getAction();        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;        if ((viewFlags & ENABLED_MASK) == DISABLED) {            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {                setPressed(false);            }            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;            // A disabled view that is clickable still consumes the touch            // events, it just doesn't respond to them.            return clickable;        }        if (mTouchDelegate != null) {            if (mTouchDelegate.onTouchEvent(event)) {                return true;            }        }        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {            switch (action) {                case MotionEvent.ACTION_UP:                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;                    if ((viewFlags & TOOLTIP) == TOOLTIP) {                        handleTooltipUp();                    }                    if (!clickable) {                        removeTapCallback();                        removeLongPressCallback();                        mInContextButtonPress = false;                        mHasPerformedLongPress = false;                        mIgnoreNextUpEvent = false;                        break;                    }                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;                    if ((mPrivateFlags & PFLAG_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 (prepressed) {                            // The button is being released before we actually                            // showed it as pressed.  Make it show the pressed                            // state now (before scheduling the click) to ensure                            // the user sees it.                            setPressed(true, x, y);                        }                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {                            // 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)) {                                    performClick();                                }                            }                        }                        if (mUnsetPressedState == null) {                            mUnsetPressedState = new UnsetPressedState();                        }                        if (prepressed) {                            postDelayed(mUnsetPressedState,                                    ViewConfiguration.getPressedStateDuration());                        } else if (!post(mUnsetPressedState)) {                            // If the post failed, unpress right now                            mUnsetPressedState.run();                        }                        removeTapCallback();                    }                    mIgnoreNextUpEvent = false;                    break;                case MotionEvent.ACTION_DOWN:                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;                    }                    mHasPerformedLongPress = false;                    if (!clickable) {                        checkForLongClick(0, x, y);                        break;                    }                    if (performButtonActionOnTouchDown(event)) {                        break;                    }                    // Walk up the hierarchy to determine if we're inside a scrolling container.                    boolean isInScrollingContainer = isInScrollingContainer();                    // For views inside a scrolling container, delay the pressed feedback for                    // a short period in case this is a scroll.                    if (isInScrollingContainer) {                        mPrivateFlags |= PFLAG_PREPRESSED;                        if (mPendingCheckForTap == null) {                            mPendingCheckForTap = new CheckForTap();                        }                        mPendingCheckForTap.x = event.getX();                        mPendingCheckForTap.y = event.getY();                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());                    } else {                        // Not inside a scrolling container, so show the feedback right away                        setPressed(true, x, y);                        checkForLongClick(0, x, y);                    }                    break;                case MotionEvent.ACTION_CANCEL:                    if (clickable) {                        setPressed(false);                    }                    removeTapCallback();                    removeLongPressCallback();                    mInContextButtonPress = false;                    mHasPerformedLongPress = false;                    mIgnoreNextUpEvent = false;                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;                    break;                case MotionEvent.ACTION_MOVE:                    if (clickable) {                        drawableHotspotChanged(x, y);                    }                    // Be lenient about moving outside of buttons                    if (!pointInView(x, y, mTouchSlop)) {                        // Outside button                        // Remove any future long press/tap checks                        removeTapCallback();                        removeLongPressCallback();                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {                            setPressed(false);                        }                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;                    }                    break;            }            return true;        }        return false;    }复制代码
  1. 一个事件序列所有的事件都只能由一个 View 完成,也就是谁当一个View决定拦截一个事件后,那么系统会将所有的事件方法分配给它处理,因此不会再调用这个 View 的 onInterceptEvent 去询问他是否要拦截了
  2. 如果同序列事件传递给一个View处理,那么它就必须消耗掉
  3. View 的 onTouchEvent 默认都会消耗事件,除非他是不可点击的
  • View 的 longClickable 属性是 false; Button clickable 是 true , TextView 的 clickable 默认是 false
  1. 事件的传递都是由外向内传递的,即事件总是先传递给父元素,然后再由父元素分发给子View.通过 requestDisallowInterceptTouchEvent 方法 可以在 子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 除外
@Override    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);        }    }复制代码

②. 事件分发源码分析

点击事件 用 MotionEvent 表示,当一个点击事件发生以后,事件先传递给 Activity,由Activity 的 dispatchEvent 进行事件派发,具体的工作由Activity的 window 完成,window 将事件传递给 dector view,dector view 一般就是当前界面的底层容器(即 setContentView 所设置的 View的父容器),通过Activity.getWindow. getDectorView()可以获得

i.Activty对点击事件的分发过程

public boolean dispatchTouchEvent(MotionEvent event) {         if (event.isTargetAccessibilityFocus()) {             if (!isAccessibilityFocusedViewOrHost()) {                return false;            }             event.setTargetAccessibilityFocus(false);        }        boolean result = false;        if (mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onTouchEvent(event, 0);        }        final int actionMasked = event.getActionMasked();        if (actionMasked == MotionEvent.ACTION_DOWN) {             stopNestedScroll();        }        if (onFilterTouchEventForSecurity(event)) {            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {                result = true;            }                         ListenerInfo li = mListenerInfo;            if (li != null && li.mOnTouchListener != null                    && (mViewFlags & ENABLED_MASK) == ENABLED                    && li.mOnTouchListener.onTouch(this, event)) {                result = true;            }            if (!result && onTouchEvent(event)) {                result = true;            }        }        if (!result && mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);        }         if (actionMasked == MotionEvent.ACTION_UP ||                actionMasked == MotionEvent.ACTION_CANCEL ||                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {            stopNestedScroll();        }        return result;    }复制代码

ii.window将事件传递给Activity的过程

public abstract boolean superDispatchTouchEvent(MotionEvent event);复制代码

iii.顶级View对点击事件的分发过程

TODO: 难度系数太高,参考<<Android艺术与探索>> page146 ~ 151

iv.view对点击事件的处理过程

TODO: 难度系数太高,参考<<Android艺术与探索>> page151 ~ 154

八.View的滑动冲突

  • 外部滑动方向和内部滑动方向的不一致 ViewPager 和 Fragment 配合使用所组成的页面滑动效果,主流应用几乎都会使用这个效果.在这种效果中,可以左右滑动来切换页面,而每个页面往往又是一个ListView.本来这种情况下是有滑动冲突的,ViewPager 内部处理了这种冲突,因此采用Viewpager时我们无须关注这个问题,如果我们采用不是ViewPager 而是 ScroolView 等,那就必须处理滑动冲突了,否则造成的后果就是内外两层只能有一层能够滑动,这是滑动因为,这是因为两者之间的滑动事件有冲突,除了这两种情况,还存在其他情况,比如外部上下滑动,内部左右滑动等,它们属于同类一类滑动冲突
  • 外部滑动方向和内部滑动方向的一致性 当手指滑动用户无法知道到底是想让哪一层滑动,所以当手指滑动就会出现问题,系统不知道用户到底是王哪一层滑动,所以当手指滑动就会出现一种问题,要么只有一层滑动,要么就是两层滑动就会很卡顿
  • 上述两种情况均存在 如:外部有一个SlideMenu效果,然后内部有一个ViewPage,ViewPage的每一个页面中又是一个ListView,但是他是几个单一的滑动事件的总合

九.常见的滑动冲突场景

TODO: 难度系数太高,参考<<Android艺术与探索>> page155 ~ 156

①滑动冲突的处理规则

TODO: 难度系数太高,参考<<Android艺术与探索>> page156 ~ 157

②滑动冲突的解决方式

TODO: 难度系数太高,参考<<Android艺术与探索>> page158 ~ 159

i.外部拦截法

ii.内部拦截法

TODO: 难度系数太高,参考<<Android艺术与探索>> page159 ~ 173

转载地址:http://funkx.baihongyu.com/

你可能感兴趣的文章
JAVA的对象复制
查看>>
打开Office报错
查看>>
我的友情链接
查看>>
AsyncTask简易使用
查看>>
关于PHP sessions的超时设置
查看>>
HAProxy负载均衡原理及企业级实例部署haproxy集群
查看>>
开源中国动弹客户端实践(三)
查看>>
Win 8创造颠覆性体验:预览版关键更新
查看>>
vim在多文件中复制粘贴内容
查看>>
Android ContentObserver
查看>>
文章“关于架构优化和设计,架构师必须知道的事情”
查看>>
疯狂java学习笔记1002---非静态内部类
查看>>
ISA2006实战系列之一:实战ISA三种客户端部署方案(上)
查看>>
TCP服务器
查看>>
U-Mail邮件系统与泛微OA系统一体化操作指南
查看>>
AC旁挂三层交换机管理ap,二层接入ap心得
查看>>
JS中比较数字大小
查看>>
springcloud 学习-eureka搭建-为eureka添加认证
查看>>
jQuery插件的开发
查看>>
基础,基础,还是基础之JAVA基础
查看>>