先看实现效果,上图:
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,则执行点击。这里仿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;
}
而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 {
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,就是服务异常,发布不了。保存在草稿箱中也可以正常预览,就是不能正常发布。无奈只好转成普通模式,代码都一块一块贴进来。郁闷....