android自定義view之實現(xiàn)三角尺功能

0.前言

小編好久沒寫博客了,由于業(yè)務(wù)需要這幾天遇到了一個難題困擾了小編很久,最后還是解決了,覺得有必要寫一下。沒錯,就是實現(xiàn)三角板的功能,而且還是可移動的哦。

1.需求分析

實現(xiàn)一個可移動的三角尺,要求可以根據(jù)需要旋轉(zhuǎn)并且能夠當(dāng)作作圖工具進(jìn)行作圖。由于要求可移動并且能當(dāng)作工具使用,也就是說三角板的view需要位于作圖view的上層,因此可以使用PopupWindow實現(xiàn);由于三角板是三角圖形的,非矩形,而我們知道android的控件是矩形的,因此必須解決事件處理的問題。
話不多說,直接上代碼吧

1.PopupWindow

首先是實現(xiàn)PopupWindow:
父類

protected PopupWindow window;
protected View view;
public void createTRiangleWindow(int width,int height)
    {
        this.width=width;this.height=height;
        assControls();
        setOnClickListen();
        createTriangleWindow(width,height);
    }

 public void showTriangleWindow(int x,int y)
    {
        px = x;
        py = y;//如果需要打開二級菜單,在同個位置打開;
        setType();
        window.showAtLocation(theApp.getDrawManager().getDrawArea(), Gravity.LEFT | Gravity.TOP,px,py);
    }

父類維護(hù)了一個PopuWindow
接下來是子類具體實現(xiàn):

package com.donview.board.ui.PopupWindow


class TriangleProtratorWindow(val con: Context) : PopupWindowEx(con) {
    private var rl_parent:RelativeLayout?=null
    private var iv_ball_top:ImageView?=null
    private var iv_ball_bottom_left:ImageView?=null
    private var iv_ball_bottom_right:ImageView?=null
    protected var lastX: Int = 0
    protected var lastY: Int = 0
    private var oriLeft: Int = 0
    private var oriRight:Int = 0
    private var oriTop:Int = 0
    private var oriBottom:Int = 0
    //初始的旋轉(zhuǎn)角度
    private var oriRotation = 0f
    private val TAG="TrianglePrint"
    private var ib_triangle: ImageView?=null
    val points= ArrayList<Point>()
    private var screenWidth=0
    private var screenHeight=0
    private var density: Float?=null
    private var singlePointId: Int = 0

//代碼塊1
    override fun assControls() {
        view = LayoutInflater.from(theApp).inflate(R.layout.triangle_layout, null)
        ib_triangle=view!!.findViewById(R.id.ib_triangle)
        rl_parent=view!!.findViewById(R.id.rl_parent)
        iv_ball_top=view!!.findViewById(R.id.iv_ball_top)
        iv_ball_bottom_left=view!!.findViewById(R.id.iv_ball_bottom_left)
        iv_ball_bottom_right=view!!.findViewById(R.id.iv_ball_bottom_right)
        val mWindowManager = theApp.getApplicationContext().getSystemService(Context.WINDOW_SERVICE) as WindowManager
        val mDisplay = mWindowManager.getDefaultDisplay()
        val mDisplayMetrics = DisplayMetrics()
        mDisplay.getRealMetrics(mDisplayMetrics)
        density = mDisplayMetrics.density
        screenWidth = if (density!!.toDouble() == 1.5) DEFAULT_4K_SCREEN_WIDTH else mDisplayMetrics.widthPixels
        screenHeight = if (density!!.toDouble() == 1.5) DEFAULT_4K_SCREEN_HEIGHT else mDisplayMetrics.heightPixels
    }

//設(shè)置觸摸事件
    override fun setOnClickListen() {
        view!!.setOnTouchListener(MyOnTouchListener())//為整個view點擊觸摸事件
    }

    private var isResolve=true
//代碼3
    inner class MyOnTouchListener : View.OnTouchListener {
        var orgX: Int = 0
        var orgY:Int = 0
        override fun onTouch(v: View, event: MotionEvent): Boolean {
            val action = event.action and MotionEvent.ACTION_MASK
            //獲取控件在屏幕的位置
            val location =  IntArray(2);
            view!!.getLocationOnScreen(location);
            //控件相對于屏幕的x與y坐標(biāo)
            val xL = location[0];
            val yL = location[1];
            val xEvent = event.x//落腳點坐標(biāo)
            val yEvent = event.y
            val screenX=xEvent+xL
            val screenY=yEvent+yL
            val point=Point(screenX.toDouble(),screenY.toDouble())
            val inside=isInside(point)
          
            if (action == MotionEvent.ACTION_DOWN) {
                oriLeft = v.left
                oriRight = v.right
                oriTop = v.top
                oriBottom = v.bottom
                lastY = event.rawY.toInt()
                lastX = event.rawX.toInt()
                oriRotation = v.rotation
                Log.d(TAG, "ACTION_DOWN: $oriRotation")
            }
            if (show)delDrag(rl_parent!!, event, action)
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    if (inside){
//點擊的是三角圖形內(nèi)部
//記錄落點位置,用于實現(xiàn)移動
                        orgX = event.getX().toInt()
                        orgY = event.getY().toInt()
                        isResolve=false//內(nèi)部點擊不需要處理繪畫事件
                    }else{
                        isResolve=true
                    }
                    if (isResolve){
                       //省略業(yè)務(wù)代碼,主要是進(jìn)行圖形繪制
                    }
                    LogUit.printD("TouchPrint", "按下")
                }
                MotionEvent.ACTION_POINTER_DOWN->{
                    if (isResolve){
                        //省略業(yè)務(wù)代碼,主要是進(jìn)行圖形繪制
                    }
                }
                MotionEvent.ACTION_MOVE->{
                    if (isResolve){
                        //省略業(yè)務(wù)代碼,主要是進(jìn)行圖形繪制
                        
                    }else{
                        offsetX = event.getRawX().toInt() - orgX
                        offsetY = event.getRawY().toInt() - orgY
//實現(xiàn)隨手指移動窗口
                        window.update(offsetX, offsetY, if (width == 0) ViewGroup.LayoutParams.WRAP_CONTENT else width,
                                if (height == 0) ViewGroup.LayoutParams.WRAP_CONTENT else height, true)
                    }
                }
                MotionEvent.ACTION_POINTER_UP->{
                    if (isResolve){
                        //省略業(yè)務(wù)代碼,主要是進(jìn)行圖形繪制
                  }
                }
                MotionEvent.ACTION_UP -> {
                    if (isResolve){
                        //省略業(yè)務(wù)代碼,主要是進(jìn)行圖形繪制
                    }
                    else if (inside){
                        showOrHide()//顯示和隱藏頂點
                    }
                    
            }
           
            return true
        }

    }

    private var show=false
//顯示和隱藏三個頂點
    private fun showOrHide() {
        show=!show
        iv_ball_top!!.visibility=if (show) View.VISIBLE else View.INVISIBLE
        iv_ball_bottom_left!!.visibility=if (show) View.VISIBLE else View.INVISIBLE
        iv_ball_bottom_right!!.visibility=if (show) View.VISIBLE else View.INVISIBLE
    }

//判斷落點是否位于三個頂點所圍成的三角形內(nèi)部
    private fun isInside(point: Point): Boolean {
        val location1 =  IntArray(2);
        iv_ball_top!!.getLocationOnScreen(location1);
        //控件相對于屏幕的x與y坐標(biāo)
        val xL1 = location1[0];
        val yL1 = location1[1];

        val location2 =  IntArray(2);
        iv_ball_bottom_left!!.getLocationOnScreen(location2);
        //控件相對于屏幕的x與y坐標(biāo)
        val xL2 = location2[0];
        val yL2 = location2[1];

        val location3 =  IntArray(2);
        iv_ball_bottom_right!!.getLocationOnScreen(location3);
        //控件相對于屏幕的x與y坐標(biāo)
        val xL3 = location3[0];
        val yL3 = location3[1];
   
        points.clear()
        points.add(Point(xL1.toDouble(),yL1.toDouble()))
        points.add(Point(xL2.toDouble(),yL2.toDouble()))
        points.add(Point(xL3.toDouble(),yL3.toDouble()))
        return isPolygonContainsPoint(points,point)
    }

//判斷點point是否落在集合mPoints圍成的多邊形內(nèi)
 public boolean isPolygonContainsPoint(List<Point> mPoints, Point point) {
        int nCross = 0;
        for (int i = 0; i < mPoints.size(); i++) {
            Point p1 = mPoints.get(i);
            Point p2 = mPoints.get((i + 1) % mPoints.size());
            // 取多邊形任意一個邊,做點point的水平延長線,求解與當(dāng)前邊的交點個數(shù)
            // p1p2是水平線段,要么沒有交點,要么有無限個交點
            if (p1.getY() == p2.getY())
                continue;
            // point 在p1p2 底部 --> 無交點
            if (point.getY() < Math.min(p1.getY(), p2.getY()))
                continue;
            // point 在p1p2 頂部 --> 無交點
            if (point.getY() >= Math.max(p1.getY(), p2.getY()))
                continue;
            // 求解 point點水平線與當(dāng)前p1p2邊的交點的 X 坐標(biāo)
            double x = (point.getY() - p1.getY()) * (p2.getX() - p1.getX()) / (p2.getY() - p1.getY()) + p1.getX();
            if (x > point.getX()) // 當(dāng)x=point.x時,說明point在p1p2線段上
                nCross++; // 只統(tǒng)計單邊交點
        }
        // 單邊交點為偶數(shù),點在多邊形之外 ---
        return (nCross % 2 == 1);
    }

  //獲取旋轉(zhuǎn)角,旋轉(zhuǎn)view
    private fun delDrag(v: View, event: MotionEvent, action: Int) {
        when (action) {
            MotionEvent.ACTION_MOVE -> {
                val dx = event.rawX.toInt() - lastX
                val dy = event.rawY.toInt() - lastY
                val center = android.graphics.Point(oriLeft + (oriRight - oriLeft) / 2, oriTop + (oriBottom - oriTop) / 2)
                val first = android.graphics.Point(lastX, lastY)
                val second = android.graphics.Point(event.rawX.toInt(), event.rawY.toInt())
                oriRotation += angle(center, first, second)

                v.rotation = oriRotation
                lastX = event.rawX.toInt()
                lastY = event.rawY.toInt()
                Log.i(TAG, "ACTION_MOVE")
            }
        }
    }

    fun angle(cen: android.graphics.Point, first: android.graphics.Point, second: android.graphics.Point): Float {
        val dx1: Float
        val dx2: Float
        val dy1: Float
        val dy2: Float

        dx1 = (first.x - cen.x).toFloat()
        dy1 = (first.y - cen.y).toFloat()
        dx2 = (second.x - cen.x).toFloat()
        dy2 = (second.y - cen.y).toFloat()

        // 計算三邊的平方
        val ab2 = ((second.x - first.x) * (second.x - first.x) + (second.y - first.y) * (second.y - first.y)).toFloat()
        val oa2 = dx1 * dx1 + dy1 * dy1
        val ob2 = dx2 * dx2 + dy2 * dy2

        // 根據(jù)兩向量的叉乘來判斷順逆時針
        val isClockwise = (first.x - cen.x) * (second.y - cen.y) - (first.y - cen.y) * (second.x - cen.x) > 0

        // 根據(jù)余弦定理計算旋轉(zhuǎn)角的余弦值
        var cosDegree = (oa2 + ob2 - ab2) / (2.0 * Math.sqrt(oa2.toDouble()) * Math.sqrt(ob2.toDouble()))

        // 異常處理,因為算出來會有誤差絕對值可能會超過一,所以需要處理一下
        if (cosDegree > 1) {
            cosDegree = 1.0
        } else if (cosDegree < -1) {
            cosDegree = -1.0
        }

        // 計算弧度
        val radian = Math.acos(cosDegree)

        // 計算旋轉(zhuǎn)過的角度,順時針為正,逆時針為負(fù)
        return (if (isClockwise) Math.toDegrees(radian) else -Math.toDegrees(radian)).toFloat()

    }
}

布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <RelativeLayout
        android:id="@+id/rl_parent"
        android:layout_marginTop="70dp"
        android:layout_marginBottom="70dp"
        android:layout_marginStart="100dp"
        android:layout_marginEnd="100dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <ImageView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/ib_triangle"
            android:src="@drawable/trangle_contain_protractor"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        </ImageView>
        <ImageView
            android:visibility="invisible"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="50dp"
            android:background="@android:color/white"
            android:id="@+id/iv_ball_top"
            android:layout_width="10dp"
            android:layout_height="10dp" />
        <ImageView
            android:visibility="invisible"
            android:layout_alignParentBottom="true"
            android:background="@android:color/white"
            android:id="@+id/iv_ball_bottom_left"
            android:layout_marginBottom="50dp"
            android:layout_width="10dp"
            android:layout_height="10dp" />
        <ImageView
            android:visibility="invisible"
            android:layout_marginBottom="50dp"
            android:layout_alignParentBottom="true"
            android:layout_alignParentEnd="true"
            android:background="@android:color/white"
            android:id="@+id/iv_ball_bottom_right"
            android:layout_width="10dp"
            android:layout_height="10dp" />
    </RelativeLayout>

</RelativeLayout>

Point類如下:

public class Point {
    private double x;//X坐標(biāo)
    private double y;//y坐標(biāo)

    public Point(double x,double y){
        setX(x);
        setY(y);
    }

    public double getY() {
        return y;
    }

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
    }

    public void setY(double y) {
        this.y = y;
    }

    @Override
    public String toString() {
        return "x="+x+",y="+y;
    }
}

ok,代碼就先貼在這里了,接下來就是分析了
代碼有點多,接下來就根據(jù)各項需求來進(jìn)行分析吧

  • 隨手勢移動
    首先是實現(xiàn)手勢移動。實現(xiàn)手勢移動的功能很簡單,只需要在事件
    ACTION_DOWN時獲取落腳點的位置:
                     orgX = event.getX().toInt()
                       orgY = event.getY().toInt()

然后在移動的時候獲取位置差計算出距離即可:

 offsetX = event.getRawX().toInt() - orgX
                        offsetY = event.getRawY().toInt() - orgY

然后再刷新當(dāng)前window的位置就可以了

window.update(offsetX, offsetY, if (width == 0) ViewGroup.LayoutParams.WRAP_CONTENT else width,
                                if (height == 0) ViewGroup.LayoutParams.WRAP_CONTENT else height, true)
  • 三角尺的控件
    這是個難點。為什么說時難點呢?因為我們知道安卓的控件是矩形的,就算你吧他的背景圖片設(shè)置成其他圖形的,他仍然是矩形控件,小編一開始采取的方法是自定義控件,最后以失敗告終。最后終于在網(wǎng)上的幾篇博客上受到啟發(fā):竟然無法實現(xiàn)三角形控件,那我們能不能做到只對三角圖形內(nèi)部的區(qū)域響應(yīng)事件呢?這樣就可以達(dá)到欺騙用戶的效果。說做就做。
    由此引發(fā)了幾個問題如下:
  • 1.首先我們需要確定落點是否在多邊形內(nèi)部
  • 2.旋轉(zhuǎn)圖形后,必須保證仍然可以正確確定落點是否在多邊形內(nèi)部
    首先是確定落點是否在多邊形內(nèi)部的問題,這個在網(wǎng)上可以找方法,這里小編就直接引用網(wǎng)上的方法了:
 /**
     * 返回一個點是否在一個多邊形區(qū)域內(nèi)
     *
     * @param mPoints 多邊形坐標(biāo)點列表
     * @param point   待判斷點
     * @return true 多邊形包含這個點,false 多邊形未包含這個點。
     */
    public static boolean isPolygonContainsPoint(List<Point> mPoints, Point point) {
        int nCross = 0;
        for (int i = 0; i < mPoints.size(); i++) {
            Point p1 = mPoints.get(i);
            Point p2 = mPoints.get((i + 1) % mPoints.size());
            // 取多邊形任意一個邊,做點point的水平延長線,求解與當(dāng)前邊的交點個數(shù)
            // p1p2是水平線段,要么沒有交點,要么有無限個交點
            if (p1.getY() == p2.getY())
                continue;
            // point 在p1p2 底部 --> 無交點
            if (point.getY() < Math.min(p1.getY(), p2.getY()))
                continue;
            // point 在p1p2 頂部 --> 無交點
            if (point.getY() >= Math.max(p1.getY(), p2.getY()))
                continue;
            // 求解 point點水平線與當(dāng)前p1p2邊的交點的 X 坐標(biāo)
            double x = (point.getY() - p1.getY()) * (p2.getX() - p1.getX()) / (p2.getY() - p1.getY()) + p1.getX();
            if (x > point.getX()) // 當(dāng)x=point.x時,說明point在p1p2線段上
                nCross++; // 只統(tǒng)計單邊交點
        }
        // 單邊交點為偶數(shù),點在多邊形之外 ---
        return (nCross % 2 == 1);
    }

傳入多邊形的頂點的集合和落腳點即可。
由于業(yè)務(wù)需要,小編這里必須計算當(dāng)前點在整個屏幕的位置,計算方法也很簡單,即點擊點的坐標(biāo)+控件相對于屏幕的位置即可。具體看代碼。
接下來是第二個問題,首先是實現(xiàn)旋轉(zhuǎn)的功能,具體方法如下(從網(wǎng)上找到的方法):
首先是在按下時獲取當(dāng)前view的位置:

 oriLeft = v.left
                oriRight = v.right
                oriTop = v.top
                oriBottom = v.bottom
                lastY = event.rawY.toInt()
                lastX = event.rawX.toInt()
                oriRotation = v.rotation

記錄上一個點的坐標(biāo)是為了計算兩點直接的角度
然后在移動的時候計算角度:

val dx = event.rawX.toInt() - lastX
                val dy = event.rawY.toInt() - lastY//Y軸上移動的位置
                val center = android.graphics.Point(oriLeft + (oriRight - oriLeft) / 2, oriTop + (oriBottom - oriTop) / 2)//獲取中心點
                val first = android.graphics.Point(lastX, lastY)//第一個點
                val second = android.graphics.Point(event.rawX.toInt(), event.rawY.toInt())//第二個點
                oriRotation += angle(center, first, second)//傳遞三個點計算出夾角,額,這里是oriRotation

                v.rotation = oriRotation
                lastX = event.rawX.toInt()
                lastY = event.rawY.toInt()
                Log.i(TAG, "ACTION_MOVE")

然后再將view的rotation 設(shè)置為計算得到的rotation就行了。
必須說明的是,這里小編使用了三個小控件分別位于三角形的頂點,主要是為了確定三角形的頂點位置從而正確計算出落腳點是否再三角形內(nèi)部,然后在旋轉(zhuǎn)圖形的時候?qū)⑷菆D形與這些點一起旋轉(zhuǎn)即可。
最后再貼上具體調(diào)用的代碼:

 //三角尺
    private fun showTriangle(){
        if(triangleProtratorWindow!=null) triangleProtratorWindow!!.shutdown()
        triangleProtratorWindow=TriangleProtratorWindow(theApp!!)
        triangleProtratorWindow!!.createTRiangleWindow(DEFAULT_TRIANGLE_WIDTH, DEFAULT_TRIANGLE_HEIGHT)
        triangleProtratorWindow!!.showTriangleWindow(200, 200)
    }

再附上效果圖:


Screenshot_2020-03-06-01-16-30-607_com.miui.home.png
Screenshot_2020-03-06-01-16-39-048_com.miui.home.png

本文代碼有點多,基礎(chǔ)不好的小白可能需要多讀幾遍然后學(xué)以致用(大佬請略過~)
本文只給出思路和本人的具體實現(xiàn)方法,由于時間倉促,就不寫那么多了,有問題歡迎留言,謝謝大家

[原創(chuàng)文章,轉(zhuǎn)載請附上]http://www.itdecent.cn/p/8daa4457a77b

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 為什么選擇自定義View來做起頭?可能是因為我最有成就感的事情除了大一剛接觸編程時就進(jìn)入了ACM之外,就是...
    孤獨的傷逝閱讀 876評論 1 5
  • 參考:https://www.w3cplus.com/svg/svg-fill-features.htmlhttp...
    天煞魔獵手閱讀 2,824評論 1 3
  • 目標(biāo): 1、掌握自定義view的流程2、掌握自定義view的三個方法3、掌握自定義view實現(xiàn)方式4、掌握自定義v...
    小慧sir閱讀 141評論 0 0
  • 目標(biāo): 1、掌握自定義view的流程2、掌握自定義view的三個方法3、掌握自定義view實現(xiàn)方式4、掌握自定義v...
    Anwfly閱讀 1,880評論 2 7
  • 目標(biāo): 1、掌握自定義view的流程2、掌握自定義view的三個方法3、掌握自定義view實現(xiàn)方式4、掌握自定義v...
    煙刺痛了眼_a1b7閱讀 415評論 0 0

友情鏈接更多精彩內(nèi)容