【Android】使用自定义View完成一个有意思的牵引球特效

前言:

        前两天查资料的时候,发现有个网站的背景效果很有意思,会随机生成一些运动的点,鼠标移动会吸引点从而给人一种牵引效果的感觉,所以一时兴起,准备写一个类似的到安卓。

        本篇文章将描述如何建立这样的数学模型到代码实现,对自定义View的操作将不做过多介绍。

 

效果预览:

图一:运动元素随机产生,具有唯一的运动方向,中间为核心元素,可以吸引一定范围内的运动元素使其产生一个指向自己方向的速度

 

图二:核心元素可以随着手指触摸移动进行跟随运动,在运动过程中,将对周围的元素产生牵引效果,但一旦运动速度过快,那么就会失去牵引效果

现象已经看到了,接下来对于这种现象,进行模型建立。

模型建立:

全局依赖建立:

a)基于安卓屏幕坐标系(0,0)由左上角开始,分别指向右方和下方,我们对坐标系移动到中央位置,y轴坐标反转,建立起我们最喜爱的x,y坐标系;

b)基于安卓自定义view绘制需要不断地委托刷新屏幕进行绘制,我们建立起全局恒定刷新时间t;

(坐标系的变换如上图)

 

运动元素模型建立:

a)具有恒定的运动方向

b)具有自身的运动速度

c)可以被核心元素吸引

d)周围一定距离的元素或者核心元素都会产生相应的线条连接

e)牵引速度会距离核心元素的距离决定

 

核心元素模型建立:

a)核心元素产生于原点位置

b)移动跟随触摸移动

c)核心元素与运动元素一定距离时,会产生牵引效果

d)牵引距离是一个范围域,即由核心元素内外圆的距离差而产生

e)过快的运动,将失去牵引效果

(阴影部分才会产生指向核心元素的牵引力)

 

数学模型:

运动元素数学模型:

方向角[0,360]度,d表示

 

运动速度[1/t,n/t],这样设计主要原因为方便计算每次刷新的运动距离,默认为1/t,n属于(0,正无穷)

 

每次刷新运动距离: t * (n / t) ,这里为 t * (1/t) = 1

 

运动元素正常情况下的运动模型:

x' = x + t * (1/t) sin (d)

y' = y + t * (1/t) cos(d)

 

核心和运动元素的连线距离:Pd

 

核心元素的最大牵引距离:Pt

 

核心元素得到最大牵引速度为:N/t

 

核心元素的牵引有效域:[Pt-Pd,Pd]

 

核心元素牵引有效域对应速度分解:

(N/t) / (Pt-Pd) , 注:Pt - Pd >= 0

 

运动元素是否被牵引计算:

Pt > √((x' - cx)^2 + (y'-cy)^2) > ( Pt - Pd )

 

运动元素在牵引域对应的指向核心元素的牵引速度为:

v' = (((x' - cx)^2 + (y'-cy)^2) ) * ( (N/t) / (Pt-Pd) ) + 1/t ,注:cx,cy为核心元素的坐标,x',y'为运动元素坐标,之所以 + 1/t 是因为运动元素在被牵引时,运动到和核心元素牵引反方向时,保证无法挣脱,因为此时两个速度刚好大小相等,方向相反。

 

运动元素指向核心元素速度方向在全局坐标系中的角度:

d' = arctan ( ((x' - cx)^2 + (y'-cy)^2)  )

 

运动元素在牵引效果下的数学模型:

x' = x + t * (1/t) sin (d) + v' * t sin  ( 90 - d' )

y' = y + t * (1/t) cos(d) + v' * t cos ( 90 - d' )

即将两个相对运动方向的速度进行相对的移动距离计算,然后进行叠加,得出最终的移动结果。

当然,也可以用另外一种算法,直接使用平行四边形法则,求出一个合速度进行计算,我这里不再多费笔墨。

 

代码实现:

这里直接放下动画的全部代码:

package com.wm.gravitation;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

public class GravView extends View {
    private static final String TAG = "GravView";

    private Core mCore = new Core();    //核心元素只有一个

    private Paint mCorePaint;
    private Paint mBallPaint;
    private Paint mLinePaint;

    private int mWidth,mHeight;
    private Canvas mCanvas;

    private boolean mCoreInit = false;
    private Timer timer;

    private int mBallNum = 40;  //元素数量
    private ArrayList<Ball> mBallList = new ArrayList<>();
    private float mScreenUpdateTime = 10;//单位:ms
    private float mBallMoveSpeed = 1/mScreenUpdateTime;    //所有元素基础移动速度
    private float mBallMaxMoveRatio = 5;    //元素最大移动速度放大倍率

    private float TRACTION_VAL = 150;   //Pd
    private float TRACTION_MAX = 275;   //Pt






    public GravView(Context context) {
        super(context);
        init();
    }

    public GravView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public GravView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public GravView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init(){
        mCorePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCorePaint.setColor(Color.argb(255,255,217,230));
        mCorePaint.setStrokeWidth(10f);
        mCorePaint.setStyle(Paint.Style.STROKE);

        mBallPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBallPaint.setColor(Color.argb(255,157,230,253));
        mBallPaint.setStrokeWidth(6f);
        mBallPaint.setStyle(Paint.Style.STROKE);

        mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mLinePaint.setColor(Color.argb(255,17,81,123));
        mLinePaint.setStrokeWidth(1f);
        mLinePaint.setStyle(Paint.Style.STROKE);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = getWidth();
        mHeight = getHeight();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        this.mCanvas = canvas;
//        mCanvas.drawColor(Color.BLUE);
        axis_init();
        if (!mCoreInit){
            core_init();
            mCoreInit = true;
            Log.i(TAG, "onDraw: 初始化" + mWidth + "--" + mHeight);
        }else {
            update_view();
        }

    }

    //坐标原点移动到中间,y轴翻转
    private void axis_init(){
        mCanvas.drawColor(Color.argb(255,02,30,47));
        mCanvas.translate(mWidth/2,mHeight/2);
        mCanvas.scale(1,-1);
    }

    private void core_init(){
        mCanvas.drawPoint(mCore.getX(),mCore.getY(),mCorePaint);
    }

    private void core_move(){
        mCanvas.drawPoint(mCore.getX(),mCore.getY(),mCorePaint);

    }

    private void ball_produce(){
        if (mBallList.size() < mBallNum){
            Ball ball = new Ball(mWidth/2,mHeight/2);
            mBallList.add(ball);
        }
    }


    private void ball_move(){
        ball_produce();
        draw_line();

        for (int m = 0 ; m < mBallList.size() ; m++){
            if (mBallList.get(m).getPx() > mWidth/2
                    || mBallList.get(m).getPx() < (-mWidth/2)
                    || mBallList.get(m).getPy() > mHeight/2
                    || mBallList.get(m).getPy() < (-mHeight/2)){
                mBallList.remove(m);
                continue;
            }
            mBallList.get(m).move_base();
            mCanvas.drawPoint(mBallList.get(m).getPx(),mBallList.get(m).getPy(),mBallPaint);

        }

    }

    //计算核心是否可以牵引元素
    private void core_traction(){
        for (int m = 0 ; m < mBallList.size() ; m++){

            if (TRACTION_MAX > Math.sqrt(Math.pow((mCore.getX() - mBallList.get(m).px),2) + Math.pow((mCore.getY() - mBallList.get(m).py),2))
                    && TRACTION_VAL < Math.sqrt(Math.pow((mCore.getX() - mBallList.get(m).px),2) + Math.pow((mCore.getY() - mBallList.get(m).py),2))){
                mBallList.get(m).setTraction(true);
                mBallList.get(m).setCore_x(mCore.getX());
                mBallList.get(m).setCore_y(mCore.getY());
            }else  {
                mBallList.get(m).setTraction(false);
            }
        }
    }

    //画出各个元素之间的线
    private void draw_line(){
        for (int m = 0 ; m < mBallList.size() ;m++ ){
            for (int j = 0; j <mBallList.size() ; j++){
                if (m == j){
                    continue;
                }


                if (Math.sqrt( Math.pow((mBallList.get(m).getPx() - mBallList.get(j).getPx()),2) + Math.pow((mBallList.get(m).getPy() - mBallList.get(j).getPy()),2))
                        < TRACTION_VAL){
                    mCanvas.drawLine(mBallList.get(m).getPx(),mBallList.get(m).getPy(),mBallList.get(j).getPx(),mBallList.get(j).getPy(),mLinePaint);
                }
            }

            //核心连线使用最大牵引值
            if (Math.sqrt(Math.pow((mBallList.get(m).getPx() - mCore.getX()),2)+ Math.pow((mBallList.get(m).getPy() - mCore.getY()),2))
                    < TRACTION_MAX){
                mCanvas.drawLine(mBallList.get(m).getPx(),mBallList.get(m).getPy(),mCore.getX(),mCore.getY(),mLinePaint);
            }
        }
    }

    private void update_view(){
        core_move();
        ball_move();
        core_traction();

    }





    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){

            case MotionEvent.ACTION_MOVE:

                mCore.setRaw_x(event.getX());
                mCore.setRaw_y(event.getY());
                mCore.calc_xy();
//                Log.i(TAG, "onTouchEvent: "+ mCoreX + "--Y->" + mCoreY);
                // 手指移动
                break;
        }


        return true;
    }

    public void start_timer(){
        stop_timer();
        timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                postInvalidate();
            }
        },0,(int)mScreenUpdateTime);
    }

    public void stop_timer(){
        if (timer != null){
            timer.cancel();
        }
    }


    private class Core{
        float x =0;
        float y =0;

        float raw_x;
        float raw_y;


        private void calc_xy(){
            x = raw_x - mWidth/2;
            y = mHeight/2 - raw_y;
        }

        public float getX() {
            return x;
        }

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

        public float getY() {
            return y;
        }

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

        public float getRaw_x() {
            return raw_x;
        }

        public void setRaw_x(float raw_x) {
            this.raw_x = raw_x;
        }

        public float getRaw_y() {
            return raw_y;
        }

        public void setRaw_y(float raw_y) {
            this.raw_y = raw_y;
        }
    }

    private class Ball{
        int par_x,par_y;
        float px;
        float py;
        float direction;
        float rate = mBallMoveSpeed;
        float rate_t ;
        boolean traction = false;
        float core_x;
        float core_y;

        Ball (int par_x, int par_y){
            this.par_x = par_x;
            this.par_y = par_y;
            initX();
            initY();
            initDire();
        }

        private void move_base(){

            if (traction){
                rate_update();
                float direction_t = (float)( Math.atan((py - core_y)/(px - core_x)) * 180.0 / Math.PI);
                if ((px - core_x)> 0){
                    rate_t = -1 * rate_t;
                }
                px = px + (mScreenUpdateTime * rate) * (float) Math.sin(Math.PI / 180 * direction)
                    + (rate_t * mScreenUpdateTime) * (float) Math.sin(Math.PI / 180 * (90 - direction_t));

                py = py + (mScreenUpdateTime * rate) * (float) Math.cos(Math.PI / 180 * direction)
                    + (rate_t * mScreenUpdateTime) * (float) Math.cos(Math.PI / 180 * (90 - direction_t));

            }else  {
                px = px + (mScreenUpdateTime * rate) * (float) Math.sin(Math.PI / 180 * direction);
                py = py + (mScreenUpdateTime * rate) * (float) Math.cos(Math.PI / 180 * direction);
            }
        }

        //牵引速度计算
        private void rate_update(){
            if (traction){

                rate_t = ((float) Math.sqrt(Math.pow(core_x - px,2) + Math.pow(core_y - py,2)) - TRACTION_VAL )
                        *(mBallMaxMoveRatio * mBallMoveSpeed ) / Math.abs(TRACTION_MAX - TRACTION_VAL) + rate;

            }
        }

        private void initX(){
            //[-par_x,par_x]
            Random random = new Random();
            px = random.nextInt(par_x - (-par_x) + 1) + (-par_x);
        }

        private void initY(){
            Random random = new Random();
            py = random.nextInt(par_y - (-par_y) + 1) + (-par_y);
        }

        private void initDire(){
            Random random = new Random();
            direction = random.nextInt(360 + 1);
        }

        public float getPx() {
            return px;
        }

        public void setPx(float px) {
            this.px = px;
        }

        public float getPy() {
            return py;
        }

        public void setPy(float py) {
            this.py = py;
        }

        public float getDirection() {
            return direction;
        }

        public void setDirection(int direction) {
            this.direction = direction;
        }

        public float getRate() {
            return rate;
        }

        public void setRate(float rate) {
            this.rate = rate;
        }

        public void setTraction(boolean traction) {
            this.traction = traction;
        }

        public boolean getTraction(){
            return this.traction;
        }

        public void setCore_x(float x){
            this.core_x = x;
        }

        public float getCore_x(){
            return this.core_x;
        }

        public float getCore_y() {
            return core_y;
        }

        public void setCore_y(float core_y) {
            this.core_y = core_y;
        }
    }

}

MainActivity中调用:

package com.wm.gravitation;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;

public class MainActivity extends AppCompatActivity {
    GravView grav;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
        grav = (GravView)findViewById(R.id.grav);
        Log.i("1", "onCreate: " + 180*Math.atan(-1)/Math.PI);
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                grav.start_timer();
            }
        });

        FloatingActionButton fab_s = (FloatingActionButton) findViewById(R.id.fab_s);
        fab_s.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                grav.stop_timer();
            }
        });
    }


}

layout代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.wm.gravitation.GravView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/grav">

    </com.wm.gravitation.GravView>


    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        app:backgroundTint="?android:attr/textColorHighlight" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab_s"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|start"
        android:layout_margin="@dimen/fab_margin"
        app:backgroundTint="?android:attr/textColorHighlight" />

</android.support.design.widget.CoordinatorLayout>

结束语:

       不知道为什么,明明编辑中对gif设置了大小,但却显示时无效果;

       如果想要更真实的物理效果,可以利用物理学公式来建立,例如加入万有引力公式等元素,只是那样会更复杂,写程序也会更久,什么时候再来兴趣在写吧。     

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值