自定义View实战篇(二)实现小说翻页一 基本原理
本文实现翻页完成动画,页面阴影,内容绘制三个部分,且小说翻页目前更新至这个部分,短期内不会更新。
实现翻页完成动画
实现翻页完成动画,我们可以借助一个控件Scoller
,他可以帮我们完成动画效果。
public class PageView extends View {
private Scroller mScroller;
//....
public PageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//设置匀速滑动的Scroller
mScroller = new Scroller(context, new LinearInterpolator());
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
//.....
case MotionEvent.ACTION_UP:
//当手指弹起时,Scroller开始滚动,即调用computeScroll()方法
mScroller.startScroll((int) a.x, (int) a.y, -(width * 2), 0, 500);
break;
default:
break;
}
return true;
}
/**
* 重写此方法,然后不断更新a的位置自动完成动画
*/
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
int currX = mScroller.getCurrX();
int currY = mScroller.getCurrY();
a.x = currX;
a.y = currY;
calculationPoint(a, f);
postInvalidate();
}
}
}
绘制阴影
从上面的GIF
图可以看到,我们翻起页面后翻起的部分周围有渐变的阴影,这部分需要借助GradientDrawable
,用其绘制一个简便的矩,然后旋转即可得到
投影在
B
上面的阴影
从下图可以看出,我们以C
为定点,创建一个矩形,然后围绕C
点旋转这个矩形即可
下面的代码比较简单,值的注意的是(float) Math.toDegrees(Math.atan2(f.y - h.y, f.x - e.x));
计算角度,其中Math.atan2(y,x)
计算两点之间的弧度,y
为角度对应对边的y
高度差,x
为邻边X
的差,然后用Math.toDegrees()
将弧度转为角度即可。
private GradientDrawable mBGradientDrawable;
//创建一个线性渐变且渐变顺序为从右至左
mBGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, new int[]{0xff111111, 0x00111111});
mBGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
private void drawB(Canvas canvas) {
canvas.save();
//.....
mBGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
float afWidth = (float) Math.hypot((a.x - f.x), (a.y - f.y));//a到f的距离
float screenWidth = (float) Math.hypot(width, height);
mBGradientDrawable.setBounds((int) (c.x - afWidth / 4), (int) c.y, (int) c.x, (int) (c.y + screenWidth));
float rotateDegrees = (float) Math.toDegrees(Math.atan2(f.y - h.y, f.x - e.x));
canvas.rotate(-(rotateDegrees + 90), c.x, c.y);
mBGradientDrawable.draw(canvas);
canvas.restore();
}
A
上左右投影和B
上的投影原理相同,左边的投影是以e
为旋转点,建立渐变矩形。右边的投影是以j
为旋转点,建立渐变的矩形,然后旋转对应角度即可。
private GradientDrawable mALeftGradientDrawable;
private GradientDrawable mARightGradientDrawable;
mALeftGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, new int[]{0x80333333, 0x00333333});
mALeftGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
mARightGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[]{0x80333333, 0x00111111});
mARightGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
private void drawA(Canvas canvas) {
canvas.save();
canvas.clipPath(getPathA());
canvas.drawBitmap(mBitmapA, 0, 0, null);
float aeWidth = (float) Math.hypot((a.x - e.x), (a.y - e.y));
float afWidth = (float) Math.hypot((a.x - f.x), (a.y - f.y));
mALeftGradientDrawable.setBounds((int) e.x, (int) e.y, (int) (e.x + 40), (int) (e.y + aeWidth));
float rotateDegrees = (float) Math.toDegrees(Math.atan2(e.y - a.y, e.x - a.x));
canvas.rotate(rotateDegrees + 90, e.x, e.y);
mALeftGradientDrawable.draw(canvas);
canvas.restore();
canvas.save();
float ahWidth = (float) Math.hypot((a.x - h.x), (a.y - h.y));
mARightGradientDrawable.setBounds((int) h.x, (int) h.y, (int) (h.x + ahWidth + 40), (int) (h.y + 40));
float mDegrees = (float) Math.toDegrees(Math.atan2(a.y - h.y, a.x - h.x));
canvas.rotate(mDegrees, h.x, h.y);
mARightGradientDrawable.draw(canvas);
canvas.restore();
}
绘制文字
绘制文字除了绘制C
界面上的较为简单,我们还是观察上面的GIF
图,发现我们C
界面成镜像,怎么实现哪?
实现原理和实现上面的阴影有些相似,我们观察下面的图就明白了(我们在翻页过程中对A页面进行复制,然后进行相应的变换并不断刷新即可)。
我们用到Matrix
对Bitmap
进行变换,我们根据下图实现即可,说说旋转角度的问题,旋转的角度可以理解是先旋转90度,然后逆时针旋转对应的角度,这个角度怎么算?
我们想想之前计算投影在A
上面左边的阴影,是不是只需要将其与这条边平行,方便旋转我们可以就是那ae和a到cf底边垂直线之间的角度即可,即90+(-角度),代码在文章末尾。
自定义View小说翻页暂时就到这里了,本来计划将代码封装一下 ,整理覆盖翻页原理的,但是计划有变暂时不会花费太多时间整理写博客,后续会将完整代码整理后放在
GITHUB
上的,当然翻页需要的文本解析会有一篇文章去更新的。
截止目前,完整代码
/**
* @author Active_Loser
* @date 2018/11/18
* Content: 自定义PageView
* A: 表示当前页面
* B: 表示上一页或下一页的页面
* C: 表示翻起的页面,即当前页的背面
*/
public class PageView extends View {
private static final String TAG = "PageView";
private Path mPathA;
private Path mPathB;
private Path mPathC;
private Bitmap mBitmapA;
private Bitmap mBitmapB;
private Bitmap mBitmapC;
private Paint mPaintTxt;
/**
* 测量出view的宽高
*/
private int width, height;
private Point a, f, g, e, h, c, j, b, k, d, i;
private Scroller mScroller;
private GradientDrawable mBGradientDrawable;
private GradientDrawable mALeftGradientDrawable;
private GradientDrawable mARightGradientDrawable;
private Bitmap mBitmapD;
public PageView(Context context) {
this(context, null);
}
public PageView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public PageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
//设置匀速滑动的Scroller
mScroller = new Scroller(context, new LinearInterpolator());
}
private void init() {
a = new Point();
f = new Point();
g = new Point();
e = new Point();
h = new Point();
c = new Point();
j = new Point();
b = new Point();
k = new Point();
d = new Point();
i = new Point();
mPathA = new Path();
mPathB = new Path();
mPathC = new Path();
mPaintTxt = new Paint();
mPaintTxt.setTextSize(60);
mPaintTxt.setColor(Color.BLACK);
mBGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, new int[]{0xff111111, 0x00111111});
mBGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
mALeftGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, new int[]{0x80333333, 0x00333333});
mALeftGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
mARightGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[]{0x80333333, 0x00111111});
mARightGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
}
@SuppressLint("DrawAllocation")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getDefaultSize(600, widthMeasureSpec);
height = getDefaultSize(1000, heightMeasureSpec);
setMeasuredDimension(width, height);
f.x = width;
f.y = height;
a.x = -1;
a.y = -1;
mBitmapA = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mBitmapB = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mBitmapC = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mBitmapD = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas mCanvasA = new Canvas(mBitmapA);
mCanvasA.drawColor(Color.GREEN);
mCanvasA.drawText("优化硬件速度;\"Car Home\"程序;支持更多的屏幕分辨率;改良的用户界面;新的浏览器的用户接口和支持HTML5;新的联系人名单;更好的白色/黑色背景比率;改进Google Maps3.1.2;支持Microsoft Exchange;支持内置相机闪光灯;支持数码变焦;改进的虚拟键盘;支持蓝牙2.1;支持动态桌面的设计。", 0, height - 40, mPaintTxt);
Canvas mCanvasB = new Canvas(mBitmapB);
mCanvasB.drawColor(Color.YELLOW);
mCanvasB.drawText("优化硬件速度;\"Car Home\"程序;支持更多的屏幕分辨率;改良的用户界面;新的浏览器的用户接口和支持HTML5;新的联系人名单;更好的白色/黑色背景比率;改进Google Maps3.1.2;支持Microsoft Exchange;支持内置相机闪光灯;支持数码变焦;改进的虚拟键盘;支持蓝牙2.1;支持动态桌面的设计。", 0, height - 40, mPaintTxt);
Canvas mCanvasD = new Canvas(mBitmapD);
mCanvasD.drawColor(Color.BLUE);
Canvas mCanvasC = new Canvas(mBitmapC);
mCanvasC.drawColor(Color.BLUE);
mCanvasC.drawText("优化硬件速度;\"Car Home\"程序;支持更多的屏幕分辨率;改良的用户界面;新的浏览器的用户接口和支持HTML5;新的联系人名单;更好的白色/黑色背景比率;改进Google Maps3.1.2;支持Microsoft Exchange;支持内置相机闪光灯;支持数码变焦;改进的虚拟键盘;支持蓝牙2.1;支持动态桌面的设计。", 0, height - 40, mPaintTxt);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
a.x = x;
a.y = y;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
calculationPoint(a, f);
// if (c.x < 0) {
// calculationAByTouch();
// calculationPoint(a, f);
// }
postInvalidate();
break;
case MotionEvent.ACTION_MOVE:
calculationPoint(a, f);
// if (c.x < 0) {
// calculationAByTouch();
// calculationPoint(a, f);
// }
postInvalidate();
break;
case MotionEvent.ACTION_UP:
finishAnim();
break;
default:
break;
}
return true;
}
private void finishAnim() {
mScroller.startScroll((int) a.x, (int) a.y, -(width * 2), 0, 500);
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
int currX = mScroller.getCurrX();
int currY = mScroller.getCurrY();
a.x = currX;
a.y = currY;
calculationPoint(a, f);
postInvalidate();
}
}
/**
* 如果c点x坐标小于0,根据触摸点重新测量a点坐标
*/
private void calculationAByYouch() {
float w0 = width - c.x;
float w1 = Math.abs(f.x - a.x);
float w2 = width * w1 / w0;
a.x = Math.abs(f.x - w2);
float h1 = Math.abs(f.y - a.y);
float h2 = w2 * h1 / w1;
a.y = Math.abs(f.y - h2);
}
@Override
protected void onDraw(Canvas canvas) {
if (a.x == -1 && a.y == -1) {
calculationPoint(a, f);
drawA(canvas);
} else {
drawA(canvas);
drawC(canvas);
drawB(canvas);
}
}
/**
* 剪切A区域
*/
private void drawA(Canvas canvas) {
canvas.save();
canvas.clipPath(getPathA());
canvas.drawBitmap(mBitmapA, 0, 0, null);
float aeWidth = (float) Math.hypot((a.x - e.x), (a.y - e.y));
float afWidth = (float) Math.hypot((a.x - f.x), (a.y - f.y));
mALeftGradientDrawable.setBounds((int) e.x, (int) e.y, (int) (e.x + 40), (int) (e.y + aeWidth));
float rotateDegrees = (float) Math.toDegrees(Math.atan2(e.y - a.y, e.x - a.x));
canvas.rotate(rotateDegrees + 90, e.x, e.y);
mALeftGradientDrawable.draw(canvas);
canvas.restore();
canvas.save();
float ahWidth = (float) Math.hypot((a.x - h.x), (a.y - h.y));
mARightGradientDrawable.setBounds((int) h.x, (int) h.y, (int) (h.x + ahWidth + 40), (int) (h.y + 40));
float mDegrees = (float) Math.toDegrees(Math.atan2(a.y - h.y, a.x - h.x));
canvas.rotate(mDegrees, h.x, h.y);
mARightGradientDrawable.draw(canvas);
canvas.restore();
}
/**
* 剪切C区域
*/
private void drawC(Canvas canvas) {
canvas.save();
canvas.clipPath(getPathA());
canvas.clipPath(getPathC(), Region.Op.REVERSE_DIFFERENCE);
//设置线性渐变
canvas.drawBitmap(mBitmapD, 0,0, null);
Matrix matrix = new Matrix();
matrix.postScale(-1, 1);
matrix.postTranslate(2 * width, 0);
float rotateDegrees = (float) Math.toDegrees(Math.atan2(e.x - a.x, e.y - a.y));
matrix.postRotate(90-rotateDegrees, f.x, f.y);
matrix.postTranslate(-(f.x - a.x), -(f.y - a.y));
//设置线性渐变
canvas.drawBitmap(mBitmapC, matrix, null);
canvas.restore();
}
/**
* 剪切B区域
*/
private void drawB(Canvas canvas) {
canvas.save();
canvas.clipPath(getPathA());
canvas.clipPath(getPathC(), Region.Op.UNION);
canvas.clipPath(getPathB(), Region.Op.REVERSE_DIFFERENCE);
canvas.drawBitmap(mBitmapB, 0, 0, null);
mBGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
float afWidth = (float) Math.hypot((a.x - f.x), (a.y - f.y));//a到f的距离
float screenWidth = (float) Math.hypot(width, height);
mBGradientDrawable.setBounds((int) (c.x - afWidth / 4), (int) c.y, (int) c.x, (int) (c.y + screenWidth));
float rotateDegrees = (float) Math.toDegrees(Math.atan2(f.y - h.y, f.x - e.x));
canvas.rotate(-(rotateDegrees + 90), c.x, c.y);
mBGradientDrawable.draw(canvas);
canvas.restore();
}
/**
* 获取区域A的path
*/
private Path getPathA() {
mPathA.reset();
mPathA.lineTo(0, height);
mPathA.lineTo(c.x, c.y);
mPathA.quadTo(e.x, e.y, b.x, b.y);
mPathA.lineTo(a.x, a.y);
mPathA.lineTo(k.x, k.y);
mPathA.quadTo(h.x, h.y, j.x, j.y);
mPathA.lineTo(width, 0);
mPathA.close();
return mPathA;
}
/**
* 获取区域C的path
*/
private Path getPathC() {
mPathC.reset();
mPathC.moveTo(i.x, i.y);
mPathC.lineTo(d.x, d.y);
mPathC.lineTo(b.x, b.y);
mPathC.lineTo(a.x, a.y);
mPathC.lineTo(k.x, k.y);
mPathC.close();//闭合区域
return mPathC;
}
/**
* 获取区域B的path
*/
private Path getPathB() {
mPathB.reset();
mPathB.lineTo(0, height);
mPathB.lineTo(width, height);
mPathB.lineTo(width, 0);
mPathB.close();//闭合区域
return mPathB;
}
/**
* 计算各个点的坐标
*
* @param a a点的坐标
* @param f f点的坐标
*/
void calculationPoint(Point a, Point f) {
g.x = (a.x + f.x) / 2;
g.y = (a.y + f.y) / 2;
e.x = g.x - (f.y - g.y) * (f.y - g.y) / (f.x - g.x);
e.y = f.y;
h.x = f.x;
h.y = g.y - (f.x - g.x) * ((f.x - g.x) / (f.y - g.y));
c.x = e.x - (f.x - e.x) / 2;
c.y = f.y;
j.x = f.x;
j.y = h.y - (f.y - h.y) / 2;
b.x = (a.x + e.x) / 2;
b.y = (a.y + e.y) / 2;
k.x = (a.x + h.x) / 2;
k.y = (a.y + h.y) / 2;
d.x = ((c.x + b.x) / 2 + e.x) / 2;
d.y = ((c.y + b.y) / 2 + e.y) / 2;
i.x = ((k.x + j.x) / 2 + h.x) / 2;
i.y = ((k.y + j.y) / 2 + h.y) / 2;
}
}
总体来说,通过这种方式实现的小说翻可能会存在各种问题,这里推荐OpenGL 实现的PageFlip,有兴趣的可以参看学习,但是实际开发中我们可能会自己去做自定义控件,后面会自己写一款小说APP集成这部分功能。