前言:
前两天查资料的时候,发现有个网站的背景效果很有意思,会随机生成一些运动的点,鼠标移动会吸引点从而给人一种牵引效果的感觉,所以一时兴起,准备写一个类似的到安卓。
本篇文章将描述如何建立这样的数学模型到代码实现,对自定义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设置了大小,但却显示时无效果;
如果想要更真实的物理效果,可以利用物理学公式来建立,例如加入万有引力公式等元素,只是那样会更复杂,写程序也会更久,什么时候再来兴趣在写吧。