ViewFlow增强onItemClick功能及ViewFlow AbsListView源代码分析
先看实现效果,上图:
ViewFlow是一个非常好用的,用于不确定item个数的水平滑动切换的开源项目。
可是从github上下载的ViewFlow事实上是不支持onItemClick功能的,touch事件中并没有处理click。
那么怎样去支持onItemClick功能呢?
一、在实现前,先带着三个问题:
序号 | 问题 |
---|---|
1 | ViewFlow须要OnItemClickListener接口吗? |
2 | ListView又是怎样实现OnItemClick的呢? |
3 | OnItemClick又是怎样被调用的呢? |
1.1、问题一
从源代码中能够看出ViewFlow是继承extends AdapterView 的。而AdapterView就是通常ListView、GridView等继承的且已经定义过OnItemClickListener了。
1.2、问题二
分析ListView源代码知道其继承extends AbsListView。而AbsListView又是继承extends AdapterView。
在AbsListView中事实上是实现了OnItemClickListener了。那么接下来的步骤仅仅要变化,仿AbsListView实现OnItemClick就可以。
1.3、问题三
分析AbsListView源代码。能够发现有个方法performItemClick方法。此方法一运行,自然就运行到了OnItemClick,不多说上源代码看:
/** * Call the OnItemClickListener, if it is defined. * * @param view The view within the AdapterView that was clicked. * @param position The position of the view in the adapter. * @param id The row id of the item that was clicked. * @return True if there was an assigned OnItemClickListener that was * called, false otherwise is returned. */ public boolean performItemClick(View view, int position, long id) { if (mOnItemClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); if (view != null) { view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); } mOnItemClickListener.onItemClick(this, view, position, id); return true; } return false; }
那么仅仅要我们想办法在ViewFlow中运行performItemClick就OK了。
二、AbsListView是怎样运行performItemClick?
一般用onItemClick中比較重要的是方法入參的postion。那么怎样获取postion呢?
2.1、postion的获取
2.1.1 在AbsListView的onTouchEvent中,在MotionEvent.ACTION_DOWN时evnet.getX与event.getY,获取出x与y坐标,再依据pointToPosition方法计算出点击item的position下标。截取片断代码例如以下:
@Override public boolean onTouchEvent(MotionEvent ev) { if (!isEnabled()) { // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return isClickable() || isLongClickable(); } .... switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { switch (mTouchMode) { case TOUCH_MODE_OVERFLING: { ... break; } default: { mActivePointerId = ev.getPointerId(0); final int x = (int) ev.getX(); final int y = (int) ev.getY(); int motionPosition = pointToPosition(x, y);//计算出down的是哪个item的postion }
2.1.2 pointToPosition方法例如以下:
/** * Maps a point to a position in the list. * * @param x X in local coordinate * @param y Y in local coordinate * @return The position of the item which contains the specified point, or * {@link #INVALID_POSITION} if the point does not intersect an item. */ public int pointToPosition(int x, int y) { Rect frame = mTouchFrame; if (frame == null) {//仅仅是为了避免反复new Rect mTouchFrame = new Rect(); frame = mTouchFrame; } final int count = getChildCount(); for (int i = count - 1; i >= 0; i--) { final View child = getChildAt(i); if (child.getVisibility() == View.VISIBLE) { child.getHitRect(frame);//获取子控件在父控件坐标系中的矩形坐标 if (frame.contains(x, y)) { return mFirstPosition + i; } } } return INVALID_POSITION; }
2.2、PerformClick的运行
2.2.1 点击也是在touch里处理的。那么直接看onTouchEvent中怎样将点击关联运行的。
case MotionEvent.ACTION_UP: { switch (mTouchMode) { case TOUCH_MODE_DOWN: case TOUCH_MODE_TAP: case TOUCH_MODE_DONE_WAITING: final int motionPosition = mMotionPosition; final View child = getChildAt(motionPosition - mFirstPosition); .... //构造了PerformClick内部来用于运行点击事件 if (mPerformClick == null) { mPerformClick = new PerformClick(); } final AbsListView.PerformClick performClick = mPerformClick; performClick.mClickMotionPosition = motionPosition; performClick.rememberWindowAttachCount(); .... if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) { ... mLayoutMode = LAYOUT_NORMAL; if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { .... if (mTouchModeReset != null) { removeCallbacks(mTouchModeReset); } mTouchModeReset = new Runnable() { @Override public void run() { mTouchMode = TOUCH_MODE_REST; child.setPressed(false); setPressed(false); if (!mDataChanged) { performClick.run();//直接运行run方法 } } }; postDelayed(mTouchModeReset, ViewConfiguration.getPressedStateDuration()); } else { mTouchMode = TOUCH_MODE_REST; updateSelectorState(); } return true; } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { performClick.run();//直接运行run方法 ...2.2.2 再看看PerformClick是怎样实现的
/** * A base class for Runnables that will check that their view is still attached to * the original window as when the Runnable was created. * */ private class WindowRunnnable { //只用于推断当前即将要运行click时window是否是同一窗体,有没有由于异常情况新开的窗体了 private int mOriginalAttachCount; public void rememberWindowAttachCount() { mOriginalAttachCount = getWindowAttachCount(); } public boolean sameWindow() { return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount; } } private class PerformClick extends WindowRunnnable implements Runnable { int mClickMotionPosition; public void run() { // The data has changed since we posted this action in the event queue, // bail out before bad things happen if (mDataChanged) return; final ListAdapter adapter = mAdapter; final int motionPosition = mClickMotionPosition; if (adapter != null && mItemCount > 0 && motionPosition != INVALID_POSITION && motionPosition < adapter.getCount() && sameWindow()) { final View view = getChildAt(motionPosition - mFirstPosition); // If there is no view, something bad happened (the view scrolled off the // screen, etc.) and we should cancel the click if (view != null) {//performItemClick被运行,至此AbsListView实现了onItemClick了 performItemClick(view, motionPosition, adapter.getItemId(motionPosition)); } } } }
三、ViewFlow运行performItemClick?
3.1、postion的获取
ViewFlow的postion事实上和AbsListView的postion获取有点差别。由于ViewFlow是水平滑动而AbsListView是竖向的。
item会不在同一屏幕宽中。使用x与y坐标再遍历ChildView的矩形坐标系并不能适用。
那么怎样来获取postion呢?
翻看源代码有ViewFlow有个ViewSwitchListener,onSwitched中有对应的postion与View。
仅仅须要查看onSwitched在何处被调用,postion与view是怎样被斌值就可以。
private void postViewSwitched(int direction) { if (direction == 0) return; if (direction > 0) { // to the right mCurrentAdapterIndex++; mCurrentBufferIndex++; ... } else { // to the left mCurrentAdapterIndex--; mCurrentBufferIndex--; ... } ... if (mViewSwitchListener != null) { //通过在构造方法mLoadedViews(List<View>)初始化,mCurrentAdapterIndex当前显示的adapter中position位置 mViewSwitchListener .onSwitched(mLoadedViews.get(mCurrentBufferIndex), mCurrentAdapterIndex); } logBuffer(); }
3.2、PerformClick的运行
同AbsListView的PerformClick运行是同理,这里也是通过在0nTouchEvent的up中运行的。
@Override public boolean onTouchEvent(MotionEvent ev) { .... final int action = ev.getAction(); final float x = ev.getX(); //---------add start 获取Y坐标 final float y = ev.getY(); //---------add end switch (action) { case MotionEvent.ACTION_DOWN: ... // Remember where the motion event started mLastMotionX = x; //---------add start mLastMotionY = y; //---------add start mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING; mIsClick = true; //每次down是默认是一次点击事件,在move中有x轴或y轴的偏移时则取消是click break; case MotionEvent.ACTION_MOVE: final int deltaX = (int) (mLastMotionX - x); boolean xMoved = Math.abs(deltaX) > mTouchSlop; //---------add start 计算y移动偏移量 推断y轴是否有移动过及本次down下,是否是一次click事件 float tempDeltaX = mLastMotionX - ev.getX(); float tempdeltaY = mLastMotionY - ev.getY(); boolean isXMoved = Math.abs(tempDeltaX) > MOVE_TOUCHSLOP; boolean isYMoved = Math.abs(tempdeltaY) > MOVE_TOUCHSLOP; boolean tempIsMoved = isXMoved || isYMoved ; //xy有点偏移都觉得不是点击事件 mIsClick = !tempIsMoved; //若x与y偏移量都过小,则觉得是一次click事件 //Log.e("------->", "ACTION_MOVE tempDeltaX:"+tempDeltaX+" tempdeltaY: "+tempdeltaY+" mTouchSlop:"+mTouchSlop+" isXMoved:"+isXMoved+" isYMoved:"+isYMoved+" isClick:"+mIsClick); //---------add start ... break; case MotionEvent.ACTION_UP: .... //------------------若是一次click。则运行点击。而PerformClick的实现例如以下:这里仿AbsListView 採用PerformClick //Log.e("------->", "ACTION_UP isClick:"+mIsClick); if(mIsClick){ if (mPerformClick == null) { mPerformClick = new PerformClick(); } final ViewFlow.PerformClick performClick = mPerformClick; performClick.mClickMotionPosition = mCurrentAdapterIndex; performClick.rememberWindowAttachCount(); //记录点击时的连接窗体次数 performClick.run(); } //------------------ ..... break; .... } return true; }
/** * A base class for Runnables that will check that their view is still attached to * the original window as when the Runnable was created. * */ private class WindowRunnnable { private int mOriginalAttachCount; public void rememberWindowAttachCount() { mOriginalAttachCount = getWindowAttachCount(); //getWindowAttachCount 获取控件绑在窗体上的次数 } public boolean sameWindow() { //推断是否是同一个窗体,异常情况时界面attachWindowCount会是+1,那么此时就不是同一个窗体了。 return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount; } } private class PerformClick extends WindowRunnnable implements Runnable { int mClickMotionPosition; public void run() { // The data has changed since we posted this action in the event queue, // bail out before bad things happen //if (mDataChanged) return; final Adapter adapter = mAdapter; final int motionPosition = mClickMotionPosition; if (adapter != null && mAdapter.getCount() > 0 && motionPosition != INVALID_POSITION && motionPosition < adapter.getCount() && sameWindow()) { //final View view = getChildAt(motionPosition - mFirstPosition); // mFirstPosition不关注 final View view = mLoadedViews.get(mCurrentBufferIndex); //position及view的获取借鉴onSwitched方法 // If there is no view, something bad happened (the view scrolled off the // screen, etc.) and we should cancel the click if (view != null) { performItemClick(view, motionPosition, adapter.getItemId(motionPosition)); } } } }
至此搞定了ViewFlow的onItemClick了。
附
最后上个ViewFlow的使用演示样例Demo
public class CircleViewFlowExample extends Activity { private ViewFlow viewFlow; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setTitle(R.string.circle_title); setContentView(R.layout.circle_layout); viewFlow = (ViewFlow) findViewById(R.id.viewflow); viewFlow.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(CircleViewFlowExample.this, "CircleViewFlowExample点击了position:"+position+"的图片", 1).show(); Log.e("-----", "CircleViewFlowExample点击了position:"+position+"的图片"); } }); viewFlow.setAdapter(new ImageAdapter(this), 5); CircleFlowIndicator indic = (CircleFlowIndicator) findViewById(R.id.viewflowindic); viewFlow.setFlowIndicator(indic); Log.e("-----", "CircleViewFlowExample onCreate"); } /* If your min SDK version is < 8 you need to trigger the onConfigurationChanged in ViewFlow manually, like this */ @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); viewFlow.onConfigurationChanged(newConfig); } }
总结
先吐槽一两句:CSDN的markdown编辑的时候感觉好爽,但最后保存公布时。老是出问题。
不是timeout,就是服务异常,公布不了。
保存在草稿箱中也能够正常预览。就是不能正常公布。
无奈仅仅好转成普通模式。代码都一块一块贴进来。郁闷....