首页 > 代码库 > Android 自定义view 折线翻页

Android 自定义view 折线翻页

看了Aige的 Android翻页效果原理实现之引入折线
有些计算原理 在此留个笔记

技术分享
x、y 为 折出的三角形的 短边与长边; O(a,b)点即为触摸点
设K = w - a, L = h - b

?OMA中,由勾股定理,得出
技术分享

?OMA与 ?AOB、?APB三者之面积和 等于 梯形 MOBP的面积
技术分享
代入x,解得
技术分享
再代入触摸点(a,b) 即可求出当前对应的x、y了

有x、y现在就可以求出A点和B点的坐标了
A点(w - x, h)
B点(w, h - y)
折线出的三角形即是:以Path的moveTo到touch点,再lineTo到A、B两点,close闭合出的三角形


AB为对折线,P点对折形成的O点应该在 以(0,h)为圆心,w为半径的圆内
由Path和Region求出这个圆的范围: mRegionShortSize
如果不在范围内,那肯定是触摸的纵坐标出了问题:
if (!mRegionShortSize.contains((int)mTouchX, (int)mTouchY)) {
    /*
     如果不在则通过x坐标强行重算y坐标
     通过圆的标准方程: (x-a)^2+(y-b)^2=r^2 (a,b)为圆心 r为半径  x,y为圆弧上的一点
     y - b = Math.sqrt(r^2 - (x-a)^2)  => y = Math.sqrt(r^2 - (x-a)^2) + b
     或
     -(y - b) = Math.sqrt(r^2 - (x-a)^2) => y = -1 * Math.sqrt(r^2 - (x-a)^2) + b
      */
//  mTouchY = (float) (Math.sqrt((Math.pow(mW, 2) - Math.pow(mTouchX, 2))) + mH); // 明显值大于mH,不对
    mTouchY = (float) (-1 * Math.sqrt((Math.pow(mW, 2) - Math.pow(mTouchX, 2))) + mH);

}
这样算出来的mTouchY即是圆的轨迹上的对应横坐标的纵坐标值
(注:原文中的表达式
mPointY = (float) (Math.sqrt((Math.pow(mViewWidth, 2) - Math.pow(mPointX, 2))) - mViewHeight);  
mPointY = Math.abs(mPointY) + mValueAdded; 
 )

到目前的代码:
package com.stone.turnpage.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Region;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

/**
 * author : stone
 * email  : aa86799@163.com
 * time   : 16/9/20 11 18
 *
 * 折线翻页
 */

public class FoldTurnPageView extends View {

    private float mTouchX, mTouchY;
    private Path mPath;
    private Paint mPaint;
    private int mW, mH;
    private Region mRegionShortSize;

    public FoldTurnPageView(Context context) {
        this(context, null);
    }

    public FoldTurnPageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FoldTurnPageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mPath = new Path();
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(10);

        mRegionShortSize = new Region();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public FoldTurnPageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mTouchX = event.getX();
        mTouchY = event.getY();
        System.out.println(mTouchY);
        if (!mRegionShortSize.contains((int)mTouchX, (int)mTouchY)) {
            /*
             如果不在则通过x坐标强行重算y坐标
             通过圆的标准方程: (x-a)^2+(y-b)^2=r^2 (a,b)为圆心 r为半径  x,y为圆弧上的一点
             y - b = Math.sqrt(r^2 - (x-a)^2)  => y = Math.sqrt(r^2 - (x-a)^2) + b
             或
             -(y - b) = Math.sqrt(r^2 - (x-a)^2) => y = -1 * Math.sqrt(r^2 - (x-a)^2) + b
              */
//            mTouchY = (float) (Math.sqrt((Math.pow(mW, 2) - Math.pow(mTouchX, 2))) + mH);
            mTouchY = (float) (-1 * Math.sqrt((Math.pow(mW, 2) - Math.pow(mTouchX, 2))) + mH);

        }
        invalidate();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                break;

        }

        return true;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mW = getMeasuredWidth();
        mH = getMeasuredHeight();

        computeShortSizeRegion();


    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mPath.reset();

        float k = mW - mTouchX;
        float l = mH - mTouchY;
        float c = (float) (Math.pow(k, 2) + Math.pow(l, 2));
        float x = c / (2 * k);
        float y = c / (2 * l);

        mPath.moveTo(mTouchX, mTouchY); //O点
        mPath.lineTo(mW - x, mH); //A点
        mPath.lineTo(mW, mH - y);   //B点
        mPath.close();

        mPaint.setColor(Color.RED);
        canvas.drawPath(mPath, mPaint);

        mPaint.setColor(Color.GREEN);
        mPath.reset();
        mPath.addCircle(0, mH, mW, Path.Direction.CCW);
        canvas.drawPath(mPath, mPaint);
    }

    /**
     * 计算短边的有效区域
     */
    private void computeShortSizeRegion() {
        // 短边圆形路径对象
        Path pathShortSize = new Path();
        // 添加圆形到Path
        pathShortSize.addCircle(0, mH, mW, Path.Direction.CCW);
        RectF bounds = new RectF();
        pathShortSize.computeBounds(bounds, true);
        //region.setPath   参数Region clip,   用于裁剪
        mRegionShortSize.setPath(pathShortSize, new Region((int)bounds.left, (int)bounds.top,
                (int)bounds.right, (int)bounds.bottom));

    }
}

效果图
技术分享



自动滑动
当在view的右下宽高四分之一处,向右滑;左侧八分之一区向左滑
向右滑:
    右部区 ra = w / 4 * 3;   底部区 ba = h / 4 * 3;
    当touch-up时,touch-x>ra && touch-y>ba 说明在右滑区
    从touch点(x, y)到右下点(w,h)获得一条直线方程;此后当不断增大x,并计算y值
    根据直线方程公式解得y值    技术分享

向左滑:
    左部 la=w/4;
    从touch点(x,y)连线到(-w,h)点;同样根据直线方程求解...

关于上端多余部分,不需要绘制:
技术分享(这里改了下图:aige博文里的图,与最开始的图不一样:AB点相反)
?AMN不需要绘制
OD垂直于PA,?BMN和?BOD即是相似三角形
       BN/MN=BD/OD,所以MN=BN/BD*OD
?BQN和?BAP是相似三角形
      BN/QN=BP/AP, 所以QN=BN/BP*AP

此时的代码:
package com.stone.turnpage.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Region;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.IntDef;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;


/**
 * author : stone
 * email  : aa86799@163.com
 * time   : 16/9/20 11 18
 * <p>
 * 折线翻页
 */

public class FoldTurnPageView extends View {

    private float mTouchX, mTouchY;
    private float mTouchUpX, mTouchUpY;//touch-up 时点的坐标
    private Path mPath;
    private Paint mPaint;
    private int mW, mH;
    private Region mRegionShortSize;
    private int mBuffArea = 20; //底部缓冲区
    private float mAutoAreaRight, mAutoAreaBottom, mAutoAreaLeft;
    private boolean mIsSlide; //是否自动滑动
    private static final int LEFT_BOTTOM = 1;  //左下
    private static final int RIGHT_BOTTOM = 2; //右下

    @IntDef({LEFT_BOTTOM, RIGHT_BOTTOM})
    @Retention(RetentionPolicy.SOURCE)
    private @interface SlideDirection {}
    @SlideDirection
    private int mSlide;

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

            if (!mIsSlide) {
                return;
            }

            if (mSlide == RIGHT_BOTTOM && mTouchX < mW) {//向下滑动
                mTouchX += 10;
            /*
            根据直线方程公式:(y-y1)/(y2-y1)=(x-x1)/(x2-x1)
            y=y1+(x-x1)*(y2-y1)/(x2-x1)

            mTouchUpX <==> x1  mTouchUpY <==> y1
            mW <==> x2      mH <==> y2
            不断变化的点 mTouchX <==> x   mTouchY <==> y
                 */
                mTouchY = mTouchUpY + (mTouchX - mTouchUpX) * (mH - mTouchUpY) / (mW - mTouchUpX);
                invalidate();
                sendMessageDelayed(obtainMessage(0), 25);
            } else if (mSlide == LEFT_BOTTOM && mTouchX > -mW) {//向左滑动
                mTouchX -= 40;
                mTouchY = mTouchUpY + (mTouchX - mTouchUpX) * (mH - mTouchUpY) / (-mW - mTouchUpX);
                invalidate();
                sendMessageDelayed(obtainMessage(0), 25);
            } else {
                slideStop();
            }
        }
    };

    public FoldTurnPageView(Context context) {
        this(context, null);
    }

    public FoldTurnPageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FoldTurnPageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mPath = new Path();
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(10);

        mRegionShortSize = new Region();

//        setLayerType(LAYER_TYPE_SOFTWARE, null); //关闭硬件加速 api11以上  在manifest中关闭
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public FoldTurnPageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        mTouchX = x;
        mTouchY = y;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mHandler.removeMessages(0);
                invalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                if (x > mAutoAreaRight && y > mAutoAreaBottom) {
                    mSlide = RIGHT_BOTTOM;
                    startSlide(x, y);
                } else if (x < mAutoAreaLeft) {
                    mSlide = LEFT_BOTTOM;
                    startSlide(x, y);
                }

                break;
        }

        return true;
    }

    private void startSlide(float x, float y ) {
        mIsSlide = true;
        mTouchUpX = x;
        mTouchUpY = y;
        mHandler.sendEmptyMessage(0);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mW = getMeasuredWidth();
        mH = getMeasuredHeight();

        computeShortSizeRegion();

        mAutoAreaRight = mW / 4 * 3;
        mAutoAreaBottom = mH / 4 * 3;
        mAutoAreaLeft = mW / 8;

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);


//        canvas.clipRegion(mRegionShortSize);

        canvas.drawColor(Color.parseColor("#d8ccaa00"));

        if (!mRegionShortSize.contains((int) mTouchX, (int) mTouchY)) {
            /*
             如果不在则通过x坐标强行重算y坐标
             通过圆的标准方程: (x-a)^2+(y-b)^2=r^2 (a,b)为圆心 r为半径  x,y为圆弧上的一点
             y - b = Math.sqrt(r^2 - (x-a)^2)  => y = Math.sqrt(r^2 - (x-a)^2) + b
             或
             -(y - b) = Math.sqrt(r^2 - (x-a)^2) => y = -1 * Math.sqrt(r^2 - (x-a)^2) + b
              */
//            mTouchY = (float) (Math.sqrt((Math.pow(mW, 2) - Math.pow(mTouchX, 2))) + mH); // 使用这个明显值偏大 比mH大
            mTouchY = (float) (-1 * Math.sqrt((Math.pow(mW, 2) - Math.pow(mTouchX, 2))) + mH);
        }

        /*
        缓冲区域判断
        当B点不在PB线上,而在屏幕上方之外,这时mTouchX, 偏左
            此时 mTouchY 越接近mH ,  折线出的就不能形成?AOB了 而是一个矩形
         */
        float area = mH - mBuffArea;
        if (mTouchY >= area && !mIsSlide) {
            mTouchY = area;
        }

        float k = mW - mTouchX;
        float l = mH - mTouchY;
        float c = (float) (Math.pow(k, 2) + Math.pow(l, 2));
        float x = c / (2 * k);
        float y = c / (2 * l);

        mPath.reset();
        mPath.moveTo(mTouchX, mTouchY); //O点

        if (y > mH) {//B点超出屏幕上端
            //计算,BN边
            float bn = y - mH;
            //MN=BN/BD*OD
            float mn = bn / (y - (mH - mTouchY)) * (mW - mTouchX);
            //QN=BN/BP*AP
            float qn = bn / y * x;
            mPath.lineTo(mW - mn, 0);  //M点
            mPath.lineTo(mW - qn, 0);  //Q点
            mPath.lineTo(mW - x, mH); //A点 在底部
        } else {
            mPath.lineTo(mW, mH - y);   //B点 在右部
            mPath.lineTo(mW - x, mH); //A点 在底部
        }
        mPath.close();


        mPaint.setColor(Color.RED);
        canvas.drawPath(mPath, mPaint);

        mPaint.setColor(Color.GREEN);
        mPath.reset();
        mPath.addCircle(0, mH, mW, Path.Direction.CCW);
        canvas.drawPath(mPath, mPaint);
    }

    /**
     * 计算短边的有效区域
     */
    private void computeShortSizeRegion() {
        // 短边圆形路径对象
        Path pathShortSize = new Path();
        // 添加圆形到Path
        pathShortSize.addCircle(0, mH, mW, Path.Direction.CCW);
        RectF bounds = new RectF();
        pathShortSize.computeBounds(bounds, true);
        //region.setPath   参数Region clip,   用于裁剪
        boolean flag = mRegionShortSize.setPath(pathShortSize, new Region((int) bounds.left, (int) bounds.top,
                (int) bounds.right, (int) bounds.bottom));
//        boolean flag = mRegionShortSize.setPath(pathShortSize, new Region(0, 1920-1080, 500, 1920));
//        System.out.println(bounds + ",," + flag);
//        System.out.println(mRegionShortSize);

    }

    /**
     * 为isSlide提供对外的停止方法便于必要时释放滑动动画
     */
    public void slideStop() {
        mIsSlide = false;
    }
}

效果图:
技术分享


最后绘制上图片:当前页图片、折叠区图片、折叠露出来的下一张图
> 关于区域的计算:

if (y > mH) {//B点超出屏幕上端
            //计算,BN边
            float bn = y - mH;
            //MN=BN/BD*OD
            float mn = bn / (y - (mH - mTouchY)) * (mW - mTouchX);
            //QN=BN/BP*AP
            float qn = bn / y * x;
            mPath.lineTo(mW - mn, 0);  //M点
            mPath.lineTo(mW - qn, 0);  //Q点
            mPath.lineTo(mW - x, mH);  //A点 在底部

            /*
            生成包含折叠和下一页的路径
            OMNPA 五点
             */
            mPathFoldAndNext.lineTo(mW - mn, 0); //M
            mPathFoldAndNext.lineTo(mW, 0);      //N
            mPathFoldAndNext.lineTo(mW, mH);     //P
            mPathFoldAndNext.lineTo(mW - x, mH); //A

        } else {
            mPath.lineTo(mW, mH - y); //B点 在右部
            mPath.lineTo(mW - x, mH); //A点 在底部

            /*
            生成包含折叠和下一页的路径
            OBPA 四点
             */
            mPathFoldAndNext.lineTo(mW, mH - y);   //B点 在右部
            mPathFoldAndNext.lineTo(mW, mH);       //P点
            mPathFoldAndNext.lineTo(mW - x, mH);   //A点 在底部
        }

使用mPathFoldAndNext记录了折叠区和它相等的下一页(右侧)区


折叠区图片的绘制,比较难理解(aige在最后画了张图,看的不是很明白,纠结了几天)
技术分享
这张草图,表示短边为x的情况,通过反正弦函数计算出x与y轴的夹角d,
当平移到touch点,再旋转(90-d)时,这时绿线部分与上边的x线重合,
再通过一些canvas操作,最后即能实现折叠区绘制对应部分的水平镜像效果
另一种情况x为长边时原理类似,草图如下
技术分享

点击 完整源码传送 

Android 自定义view 折线翻页