Android自定义控件实战——仿多看阅读平移翻页
之前自己做的一个APP需要用到翻页阅读,网上看过立体翻页效果,不过bug太多了还不兼容。看了一下多看阅读翻页是采用平移翻页的,于是就仿写了一个平移翻页的控件。效果如下:
在翻页时页面右边缘绘制了阴影,效果还不错。要实现这种平移翻页控件并不难,只需要定义一个布局管理页面就可以了。具体实现上有以下难点:
1、循环翻页,页面的重复利用。
2、在翻页时过滤掉多点触碰。
3、采用setAdapter的方式设置页面布局和数据。
下面就来一一解决这几个难点。首先看循环翻页问题,怎么样能采用较少的页面实现这种翻页呢?由于屏幕上每次只能显示一张完整的页面,翻过去的页面也看不到,所以可以把翻过去的页面拿来重复利用,不必每次都new一个页面,所以,我只用了三张页面实现循环翻页。要想重复利用页面,首先要知道页面在布局中序号和对应的层次关系,比如一个父控件的子view的序号越大就位于越上层。循环利用页面的原理图如下:
向右翻页时状态图是这样的,只用了0、1、2三张页面,页面序号为2的位于最上层,我把它隐藏在左边,所以看到的只有页面1,页面0在1下面挡着也看不到,向右翻页时,页面2被滑到屏幕中,这时候把页面0的内容替换成页面2的前一页内容,把它放到之前页面2的位置,这时,状态又回到了初始状态,又可以继续向右翻页了!
向左翻页时是这样的,初始状态还是一样,当页面1被往左翻过时,看到的是页面0,这时候页面0下面已经没有页面了,而页面2已经用不到了,这时候把页面2放到页面0下面,这时候状态又回到了初始状态,就可以继续往左翻页了。
类似于这种循环效果的实现我一直用的解决方案都是将选中的置于最中间,比如原理图中的页面1,每次翻页完成后可见的都是页面1。在滚动选择器PickerView中也是同样的方案。这就解决了页面的重复利用问题了。
解决难点2 翻页时过滤多点触碰这个问题在仿淘宝商品浏览界面中已经解决过了,就是用一个控制变量mEvents过滤掉pointer down或up后到来的第一个move事件。
解决难点3 采用adapter方式设置页面的布局和数据。这个在Android的AdapterView里用到的,但是我没有看它的adapter机制,太复杂了,我就搞了个简单的adapter,如下:
PageAdapter.java:
package com.jingchen.pagerdemo; import android.view.View; public abstract class PageAdapter { /** * @return 页面view */ public abstract View getView(); public abstract int getCount(); /** * 将内容添加到view中 * * @param view * 包含内容的view * @param position * 第position页 */ public abstract void addContent(View view, int position); }这是一个抽象类,getView()用于返回页面的布局,getCount()返回数据总共需要多少页,addContent(View view, int position)这个是每翻过一页后将会被调用来请求页面数据的,参数view就是页面,position是表明第几页。待会儿会在自定义布局中定义setAdapter方法设置设配器。
OK,难点都解决了,自定义一个布局叫ScanView继承自RelativeLayout:
ScanView.java:
package com.jingchen.pagerdemo; import java.util.Timer; import java.util.TimerTask; import android.content.Context; import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.RectF; import android.graphics.Shader.TileMode; import android.os.Handler; import android.os.Message; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.widget.RelativeLayout; /** * @author chenjing * */ public class ScanView extends RelativeLayout { public static final String TAG = "ScanView"; private boolean isInit = true; // 滑动的时候存在两页可滑动,要判断是哪一页在滑动 private boolean isPreMoving = true, isCurrMoving = true; // 当前是第几页 private int index; private float lastX; // 前一页,当前页,下一页的左边位置 private int prePageLeft = 0, currPageLeft = 0, nextPageLeft = 0; // 三张页面 private View prePage, currPage, nextPage; // 页面状态 private static final int STATE_MOVE = 0; private static final int STATE_STOP = 1; // 滑动的页面,只有前一页和当前页可滑 private static final int PRE = 2; private static final int CURR = 3; private int state = STATE_STOP; // 正在滑动的页面右边位置,用于绘制阴影 private float right; // 手指滑动的距离 private float moveLenght; // 页面宽高 private int mWidth, mHeight; // 获取滑动速度 private VelocityTracker vt; // 防止抖动 private float speed_shake = 20; // 当前滑动速度 private float speed; private Timer timer; private MyTimerTask mTask; // 滑动动画的移动速度 public static final int MOVE_SPEED = 10; // 页面适配器 private PageAdapter adapter; /** * 过滤多点触碰的控制变量 */ private int mEvents; public void setAdapter(ScanViewAdapter adapter) { removeAllViews(); this.adapter = adapter; prePage = adapter.getView(); addView(prePage, 0, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); adapter.addContent(prePage, index - 1); currPage = adapter.getView(); addView(currPage, 0, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); adapter.addContent(currPage, index); nextPage = adapter.getView(); addView(nextPage, 0, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); adapter.addContent(nextPage, index + 1); } /** * 向左滑。注意可以滑动的页面只有当前页和前一页 * * @param which */ private void moveLeft(int which) { switch (which) { case PRE: prePageLeft -= MOVE_SPEED; if (prePageLeft < -mWidth) prePageLeft = -mWidth; right = mWidth + prePageLeft; break; case CURR: currPageLeft -= MOVE_SPEED; if (currPageLeft < -mWidth) currPageLeft = -mWidth; right = mWidth + currPageLeft; break; } } /** * 向右滑。注意可以滑动的页面只有当前页和前一页 * * @param which */ private void moveRight(int which) { switch (which) { case PRE: prePageLeft += MOVE_SPEED; if (prePageLeft > 0) prePageLeft = 0; right = mWidth + prePageLeft; break; case CURR: currPageLeft += MOVE_SPEED; if (currPageLeft > 0) currPageLeft = 0; right = mWidth + currPageLeft; break; } } /** * 当往回翻过一页时添加前一页在最左边 */ private void addPrePage() { removeView(nextPage); addView(nextPage, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); // 从适配器获取前一页内容 adapter.addContent(nextPage, index - 1); // 交换顺序 View temp = nextPage; nextPage = currPage; currPage = prePage; prePage = temp; prePageLeft = -mWidth; } /** * 当往前翻过一页时,添加一页在最底下 */ private void addNextPage() { removeView(prePage); addView(prePage, 0, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); // 从适配器获取后一页内容 adapter.addContent(prePage, index + 1); // 交换顺序 View temp = currPage; currPage = nextPage; nextPage = prePage; prePage = temp; currPageLeft = 0; } Handler updateHandler = new Handler() { @Override public void handleMessage(Message msg) { if (state != STATE_MOVE) return; // 移动页面 // 翻回,先判断当前哪一页处于未返回状态 if (prePageLeft > -mWidth && speed <= 0) { // 前一页处于未返回状态 moveLeft(PRE); } else if (currPageLeft < 0 && speed >= 0) { // 当前页处于未返回状态 moveRight(CURR); } else if (speed < 0 && index < adapter.getCount()) { // 向左翻,翻动的是当前页 moveLeft(CURR); if (currPageLeft == (-mWidth)) { index++; // 翻过一页,在底下添加一页,把最上层页面移除 addNextPage(); } } else if (speed > 0 && index > 1) { // 向右翻,翻动的是前一页 moveRight(PRE); if (prePageLeft == 0) { index--; // 翻回一页,添加一页在最上层,隐藏在最左边 addPrePage(); } } if (right == 0 || right == mWidth) { releaseMoving(); state = STATE_STOP; quitMove(); } ScanView.this.requestLayout(); } }; public ScanView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } public ScanView(Context context) { super(context); init(); } public ScanView(Context context, AttributeSet attrs) { super(context, attrs); init(); } /** * 退出动画翻页 */ public void quitMove() { if (mTask != null) { mTask.cancel(); mTask = null; } } private void init() { index = 1; timer = new Timer(); mTask = new MyTimerTask(updateHandler); } /** * 释放动作,不限制手滑动方向 */ private void releaseMoving() { isPreMoving = true; isCurrMoving = true; } @Override public boolean dispatchTouchEvent(MotionEvent event) { if (adapter != null) switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: lastX = event.getX(); try { if (vt == null) { vt = VelocityTracker.obtain(); } else { vt.clear(); } } catch (Exception e) { e.printStackTrace(); } vt.addMovement(event); mEvents = 0; break; case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_POINTER_UP: mEvents = -1; break; case MotionEvent.ACTION_MOVE: // 取消动画 quitMove(); Log.d("index", "mEvents = " + mEvents + ", isPreMoving = " + isPreMoving + ", isCurrMoving = " + isCurrMoving); vt.addMovement(event); vt.computeCurrentVelocity(500); speed = vt.getXVelocity(); moveLenght = event.getX() - lastX; if ((moveLenght > 0 || !isCurrMoving) && isPreMoving && mEvents == 0) { isPreMoving = true; isCurrMoving = false; if (index == 1) { // 第一页不能再往右翻,跳转到前一个activity state = STATE_MOVE; releaseMoving(); } else { // 非第一页 prePageLeft += (int) moveLenght; // 防止滑过边界 if (prePageLeft > 0) prePageLeft = 0; else if (prePageLeft < -mWidth) { // 边界判断,释放动作,防止来回滑动导致滑动前一页时当前页无法滑动 prePageLeft = -mWidth; releaseMoving(); } right = mWidth + prePageLeft; state = STATE_MOVE; } } else if ((moveLenght < 0 || !isPreMoving) && isCurrMoving && mEvents == 0) { isPreMoving = false; isCurrMoving = true; if (index == adapter.getCount()) { // 最后一页不能再往左翻 state = STATE_STOP; releaseMoving(); } else { currPageLeft += (int) moveLenght; // 防止滑过边界 if (currPageLeft < -mWidth) currPageLeft = -mWidth; else if (currPageLeft > 0) { // 边界判断,释放动作,防止来回滑动导致滑动当前页是前一页无法滑动 currPageLeft = 0; releaseMoving(); } right = mWidth + currPageLeft; state = STATE_MOVE; } } else mEvents = 0; lastX = event.getX(); requestLayout(); break; case MotionEvent.ACTION_UP: if (Math.abs(speed) < speed_shake) speed = 0; quitMove(); mTask = new MyTimerTask(updateHandler); timer.schedule(mTask, 0, 5); try { vt.clear(); vt.recycle(); } catch (Exception e) { e.printStackTrace(); } break; default: break; } super.dispatchTouchEvent(event); return true; } /* * (非 Javadoc) 在这里绘制翻页阴影效果 * * @see android.view.ViewGroup#dispatchDraw(android.graphics.Canvas) */ @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); if (right == 0 || right == mWidth) return; RectF rectF = new RectF(right, 0, mWidth, mHeight); Paint paint = new Paint(); paint.setAntiAlias(true); LinearGradient linearGradient = new LinearGradient(right, 0, right + 36, 0, 0xffbbbbbb, 0x00bbbbbb, TileMode.CLAMP); paint.setShader(linearGradient); paint.setStyle(Style.FILL); canvas.drawRect(rectF, paint); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mWidth = getMeasuredWidth(); mHeight = getMeasuredHeight(); if (isInit) { // 初始状态,一页放在左边隐藏起来,两页叠在一块 prePageLeft = -mWidth; currPageLeft = 0; nextPageLeft = 0; isInit = false; } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (adapter == null) return; prePage.layout(prePageLeft, 0, prePageLeft + prePage.getMeasuredWidth(), prePage.getMeasuredHeight()); currPage.layout(currPageLeft, 0, currPageLeft + currPage.getMeasuredWidth(), currPage.getMeasuredHeight()); nextPage.layout(nextPageLeft, 0, nextPageLeft + nextPage.getMeasuredWidth(), nextPage.getMeasuredHeight()); invalidate(); } class MyTimerTask extends TimerTask { Handler handler; public MyTimerTask(Handler handler) { this.handler = handler; } @Override public void run() { handler.sendMessage(handler.obtainMessage()); } } }代码中的注释写的非常多,原理理解了看代码就容易看懂了。写完这个布局后再写一个ScanViewAdapter继承PageAdapter:
package com.jingchen.pagerdemo; import java.util.List; import android.content.Context; import android.content.res.AssetManager; import android.graphics.Typeface; import android.view.LayoutInflater; import android.view.View; import android.widget.TextView; public class ScanViewAdapter extends PageAdapter { Context context; List<String> items; AssetManager am; public ScanViewAdapter(Context context, List<String> items) { this.context = context; this.items = items; am = context.getAssets(); } public void addContent(View view, int position) { TextView content = (TextView) view.findViewById(R.id.content); TextView tv = (TextView) view.findViewById(R.id.index); if ((position - 1) < 0 || (position - 1) >= getCount()) return; content.setText(" 双峰叠障,过天风海雨,无边空碧。月姊年年应好在,玉阙琼宫愁寂。谁唤痴云,一杯未尽,夜气寒无色。碧城凝望,高楼缥缈西北。\n\n 肠断桂冷蟾孤,佳期如梦,又把阑干拍。雾鬓风虔相借问,浮世几回今夕。圆缺睛明,古今同恨,我更长为客。蝉娟明夜,尊前谁念南陌。"); tv.setText(items.get(position - 1)); } public int getCount() { return items.size(); } public View getView() { View view = LayoutInflater.from(context).inflate(R.layout.page_layout, null); return view; } }这里只是我的demo里写的Adapter,也可以写成带更多内容的Adapter。addContent里带的参数view就是getView里面返回的view,这样就可以根据inflate的布局设置内容了,getView返回的布局page_layout.xml如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/cover" > <TextView android:id="@+id/content" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_marginTop="60dp" android:padding="10dp" android:textColor="#000000" android:textSize="22sp" /> <TextView android:id="@+id/index" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_marginBottom="60dp" android:textColor="#000000" android:textSize="30sp" /> </RelativeLayout>只包含了两个TextView,所以在adapter中可以根据id查找到这两个TextView再给它设置内容。
OK了,MainActivity的布局如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <com.jingchen.pagerdemo.ScanView android:id="@+id/scanview" android:layout_width="match_parent" android:layout_height="match_parent" /> </RelativeLayout>很简单,只包含了ScanView。
MainActivity的代码:
package com.jingchen.pagerdemo; import java.util.ArrayList; import java.util.List; import android.app.Activity; import android.os.Bundle; import android.view.Menu; public class MainActivity extends Activity { ScanView scanview; ScanViewAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); scanview = (ScanView) findViewById(R.id.scanview); List<String> items = new ArrayList<String>(); for (int i = 0; i < 8; i++) items.add("第 " + (i + 1) + " 页"); adapter = new ScanViewAdapter(this, items); scanview.setAdapter(adapter); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); return true; } }给ScanView设置Adapter就可以了。
好啦,仿多看的平移翻页就完成了~
来自:http://blog.csdn.net/zhongkejingwang/article/details/38728119