李子果 原创。。。
最近在做一个山寨版的王者荣耀,刚开始做的时候毫无头绪 摇杆的多点触控做的特别烂
经过几天的思考已完美解决所有问题,下面就和大家分享下这个摇杆的开发思路(此教程不包含游戏源码)
若有不正之处,请多多谅解并欢迎指正。
首先这个摇杆要用到较多的数学知识,小编的数学特别烂也就高中水平吧
我们这个摇杆一共就五个按钮,一个移动摇杆、三个技能摇杆和一个普通攻击按钮
新建一个项目
建好项目之后,我们先新建一个类叫做“画”。也是我们的主View
public class Hua extends RelativeLayout implements Runnable{ //继承RelativeLayout 实现Runnable接口
private Paint p;//画笔
public Hua(Context context) {
super(context);
p=new Paint();
setBackgroundColor(Color.BLACK);//背景颜色设为黑色
}
@Override
protected void onDraw(Canvas g) {//重写onDraw方法
super.onDraw(g);
}
@Override
public void run() {
}
}
我们要准备一张图片(待会我会把图片都丢在附件里)
首先用ps画一个这样半透明的圆ps部分就不做教程了
把我们刚刚制作的图片添加进来
public class my { //这个类当一个全局变量使用
public static int w,h;//屏幕的宽高
public static float bili;
public static MainActivity main;
public static RectF re=new RectF();
public static int ontouchAlpha=100;//触控区透明度0-255 0为透明,为了测试我们先设为100
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
my.main=this;
getSupportActionBar().hide();//隐藏标题栏
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);//全屏 隐藏状态栏
//判断当前是否横屏 如果不是就设为横屏,设为横屏之后会自动调用onCreate方法
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
//获取屏幕的宽高
DisplayMetrics dis = getResources().getDisplayMetrics();
my.w = dis.widthPixels;
my.h = dis.heightPixels;
//获取屏幕分辨率和1920*1080的比例 以便适应不同大小的屏幕
my.bili = (float) (Math.sqrt(my.w * my.h) / Math.sqrt(1920 * 1080));
setContentView(new Hua(this));
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);// 横屏
}
}
}
package com.yaogan.liziguo.yaogan;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
/**
* Created by Liziguo on 2018/6/15.
*/
public class Move {
private float x1,y1;//按下时的坐标 大圆
private float x2,y2;//移动后的坐标 小圆
private final float r1,r2;//r1大圆的半径 r2小圆的半径
public float angle;//x1y1指向x2y2的角度 弧度制
public boolean down=false;//判断是否被按下
public boolean in=false;//判断小圆是否在大圆里面,简单的说就是防止小圆被脱太远
public boolean move=false;//判断手指按下后是否移动(MY实际开发中用到,该教程用不到此变量)
public Bitmap img;//大圆小圆的图片
public Move(){
r1 = 480 * 0.5f * my.bili;//乘上一个比例 适应不同大小的屏幕
r2 = 300 * 0.5f * my.bili;
img= BitmapFactory.decodeResource(my.main.getResources(),R.mipmap.yaogan);//初始化摇杆图片
}
public void down(float xx,float yy){ //摇杆按下后的操作
if(xx xx)//判断x1指向x2的方向
angle=-Math.PI/2;
else
angle=Math.PI/2;
else{
k=(x1-xx)/(y1-yy); //两点的坐标求斜率,至于为什么是(x1-x2)/(y1-y2)不是(y1-y2)/(x1-x2)待会我们再做解释
if (y1 > yy) {//判断x1y1指向x2y2的方向
// 用反tan求角度 这个高中好像没学过 既然Math类已经帮我们封装好了就直接拿来用吧
angle=Math.atan(k) + Math.PI;
} else {
angle=Math.atan(k);
}
//这段可写可不写 让计算出来的角度属于-PI/2到PI/2
if(angle>Math.PI)
angle-=Math.PI*2;
else if(angle<-Math.PI)
angle+=Math.PI*2;
}
return (float) angle;
}
public boolean in(float xx, float yy) { //防止小圆被脱太远 拖动范围不超出r1的70%
double r = Math.sqrt((x1 - xx) * (x1 - xx) + (y1 - yy) * (y1 - yy));//两点间距离公式
if (r < r1*0.7f)
return true;
else return false;
}
public boolean isMove(float xx, float yy) { //判断按下摇杆后 是否移动,如果x1y1 x2y2的距离大于r1*0.15视为移动
// MY实际开发中用到,该教程用不到此变量
double r = Math.sqrt((x1 - xx) * (x1 - xx) + (y1 - yy) * (y1 - yy));//两点间距离公式
if (r > r1*0.15f)
return true;
else return false;
}
public void onDraw(Canvas g, Paint p){ //画摇杆
if(down) { //当摇杆被按下时 才显示
//怎么用Canvas画图这里就不说了
my.re.left = x1 - r1;
my.re.top = y1 - r1;
my.re.right = x1 + r1;
my.re.bottom = y1 + r1;
g.drawBitmap(img, null, my.re, p); //画大圆
my.re.left = x2 - r2;
my.re.top = y2 - r2;
my.re.right = x2 + r2;
my.re.bottom = y2 + r2;
g.drawBitmap(img, null, my.re, p); //画小圆
}
}
}
package com.yaogan.liziguo.yaogan;
import android.content.Context;
import android.graphics.Color;
import android.view.MotionEvent;
import android.view.View;
/**
* Created by Liziguo on 2018/6/16.
*/
public class OnTouchMove extends View { //这个view负责监听移动摇杆的手势
private Move m;
public OnTouchMove(Context context,Move move) {
super(context);
this.m=move;
setBackgroundColor(Color.WHITE);//背景色设为白色
getBackground().setAlpha(my.ontouchAlpha);//设置触控区透明度
setOnTouchListener(new OnTouchListener() { //设置触控监听
@Override
public boolean onTouch(View v, MotionEvent ev) {
//加上getX() getY()因为这个view不是分布在左上角的
final float xx = ev.getX() + getX(), yy = ev.getY() + getY();
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
m.down(xx, yy);//按下时的操作
// m.move(xx, yy);
}
m.move(xx, yy);//移动时的操作
if (ev.getAction() == MotionEvent.ACTION_UP) {
m.up();//松开时的操作
}
return true;//不要返回false
}
});
}
}
package com.yaogan.liziguo.yaogan;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.widget.RelativeLayout;
/**
* Created by Liziguo on 2018/6/15.
*/
public class Hua extends RelativeLayout implements Runnable{ //继承RelativeLayout 实现Runnable接口
private Paint p;//画笔
private Move m=new Move();//移动摇杆
public Hua(Context context) {
super(context);
p=new Paint();
setBackgroundColor(Color.BLACK);//背景颜色设为黑色
//实例化一个OnTouchMove
OnTouchMove onTouchMove=new OnTouchMove(context,m);
//把onTouchMove添加进来 宽度为屏幕的1/3 高度为屏幕的1/2
addView(onTouchMove,my.w/3,my.h/2);
//设置onTouchMove的位置
onTouchMove.setX(0);
onTouchMove.setY(my.h/2);
new Thread(this).start();//启动重绘线程
}
@Override
protected void onDraw(Canvas g) {//重写onDraw方法
super.onDraw(g);
m.onDraw(g,p);//画移动摇杆
}
@Override
public void run() { //每隔20毫秒刷新一次画布
while(true){
try {Thread.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
postInvalidate();//重绘 在子线程重绘不能调用Invalidate()方法
}
}
}
好的 现在我们的摇杆可以说已经做好一大半了,因为剩下的原理都一样
左下角的白色矩形是我们的OnTouchMove类,为了更好的测试我们先让他显示出来 等做好了再隐藏掉
下面我们来解释一下为什么斜率k=(x1-x2)/(y1-y2)而不是(y1-y2)/(x1-x2)吧
因为我们手机上的平面直角坐标系跟数学上的平面直角坐标系不一样
数学上的平面直角坐标系是这样的
而我们手机是这样的
有没有发现把手机的坐标系 逆时针旋转一下就是数学里的坐标系了,不过x跟y调了一下位置
所以我们在写代码的时候把x y换一下就行了
数学坐标系中k=1的直线
程序中k=1的直线
public void move(float xx,float yy){ //按下摇杆后移动的操作
angle=getAngle(xx,yy);
in=in(xx, yy);
move=isMove(xx,yy);
if (!in) {
//下面会做解释
xx= (float) (x1+ Math.sin(angle)*r1*0.7f);
yy= (float) (y1+ Math.cos(angle)*r1*0.7f);
}
x2=xx;
y2=yy;
}
不知不觉已经凌晨2:46了,今天就写到这吧
好的下面我们开始做技能摇杆,这教程做的比较累啊
下面的技能类是我直接从我游戏里拷贝过来的并做了些小修改
解释可能没那么清楚毕竟原理都一样
只不过是多了几个功能而已
准备图片
添加到工程里
由于图片比较多 我们加载图的代码位置改一下
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
my.main=this;
getSupportActionBar().hide();//隐藏标题栏
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);//全屏 隐藏状态栏
//判断当前是否横屏 如果不是就设为横屏,设为横屏之后会自动调用onCreate方法
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
//获取屏幕的宽高
DisplayMetrics dis = getResources().getDisplayMetrics();
my.w = dis.widthPixels;
my.h = dis.heightPixels;
//获取屏幕分辨率和1920*1080的比例 以便适应不同大小的屏幕
my.bili = (float) (Math.sqrt(my.w * my.h) / Math.sqrt(1920 * 1080));
//加载图片
my.border= BitmapFactory.decodeResource(my.main.getResources(),R.mipmap.border);
my.cancel= BitmapFactory.decodeResource(my.main.getResources(),R.mipmap.cancel);
my.down= BitmapFactory.decodeResource(my.main.getResources(),R.mipmap.down);
my.yaogan= BitmapFactory.decodeResource(my.main.getResources(),R.mipmap.yaogan);
my.cd= BitmapFactory.decodeResource(my.main.getResources(),R.mipmap.cd);
setContentView(new Hua(this));
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);// 横屏
}
}
}
public class my { //这个类当一个全局变量使用
public static int w,h;//屏幕的宽高
public static float bili;
public static MainActivity main;
public static RectF re=new RectF();
public static int ontouchAlpha=100;//触控区透明度0-255 0为透明,为了测试我们先设为100
public static Bitmap border,cancel,down,yaogan,cd;
public static Skill skill;//当前正在使用的技能 现在会报错 因为我们还没新建技能Skill类
}
修改 Move 类的构造方法
public Move(){
r1 = 480 * 0.5f * my.bili;//乘上一个比例 适应不同大小的屏幕
r2 = 300 * 0.5f * my.bili;
// img= BitmapFactory.decodeResource(my.main.getResources(),R.mipmap.yaogan);//初始化摇杆图片////////////////////////
img=my.yaogan;////////////////////////////////////////////////////
}
package com.yaogan.liziguo.yaogan;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
/**
* Created by Liziguo on 2018/6/16.
*/
public abstract class Skill {
public int jineng;
private final float x,y;//技能图标中心位置,不是按下时的位置
private float x2, y2;//技能按下移动后手指的坐标
private float xxx,yyy;//判断拖动点是否超出两倍r的范围
private final float calcelx, cancely;
public float angle;//技能按下后 x y指向xx yy的角度
public Bitmap img, imgborder, imgdown, imgyaogan,imgcd,imgcancel;
private final float r2;
private final float r3=50*my.bili;
public boolean down=false;
public boolean down_main=false;//down_main 只触发一次;
public boolean cancel=false;
public int cdmax;
public long last,cd=0;//last最后一次释放技能的时间
/*
0 普通攻击
1 技能1
2 技能2
3 技能3
*/
public Skill(int jineng, int cd, Bitmap image){
this.jineng=jineng;
switch (jineng){
case 0:
x= my.w*0.87f;
y= my.h*0.8f;
break;
case 1:
x= my.w*0.7f;
y= my.h*0.88f;
break;
case 2:
x= my.w*0.75f;
y= my.h*0.62f;
break;
case 3:
x= my.w*0.9f;
y= my.h*0.5f;
break;
default:x=y=0;
}
cdmax=cd;
if(jineng == 0) r2=125*my.bili;
else r2=80*my.bili;
calcelx =my.w-r2*2;
cancely =my.h/4;
img=image;
imgborder=my.border;
imgdown=my.down;
imgyaogan=my.yaogan;
imgcd=my.cd;
imgcancel=my.cancel;
}
// public abstract void down();
// public abstract void move();
// public abstract void up();
public void down(){ //DOWN 由ontouch触发
if(cd>0)return;
down=true;
my.skill=this;
}
public abstract void down_main(); //DOWN 教程用不到该抽象方法
public void move(float x,float y){//按下技能后 由ontouch触发
x2 =x;
y2 =y;
angle=getAngle(x2, y2);
cancel=incancel(x,y);
if (jineng !=0 && !in2(x,y)) {
xxx= (float) (this.x+ Math.sin(angle)*r2*2);
yyy= (float) (this.y+ Math.cos(angle)*r2*2);
}else{
xxx=x;
yyy=y;
}
}
public abstract void move_main();//按下技能后 由MyActor触发 教程用不到该抽象方法
public abstract void up(); //松开后 由MyActor触发 释放技能
public boolean in(float xx,float yy){ //判断是否被点中
double r= Math.sqrt((x - xx)*(x-xx)+(y-yy)*(y-yy));
if(r xx)
angle= (float) (-Math.PI/2);
else
angle= (float) (Math.PI/2);
else{
k=(x-xx)/(y-yy);
if (y > yy) {
angle= (float) (Math.atan(k) + Math.PI);
} else {
angle= (float) Math.atan(k);
}
if(angle>Math.PI)
angle-=Math.PI*2;
else if(angle<-Math.PI)
angle+=Math.PI*2;
}
return angle;
}
private float drawpx=10*my.bili;
public void next(){
//计算技能冷却时间
cd=cdmax-System.currentTimeMillis()+last;
}
//按下的时候技能图标下移 显示蓝色框框
public void onDraw(Canvas g, Paint p){
my.re.left=x-r2;
my.re.top=y-r2;
my.re.right=x+r2;
my.re.bottom=y+r2;
if(down){
// new RectF(x-r2,y-r2,x+r2,y+r2);
// new RectF(x-r2,y-r2+10*my.bili,x+r2,y+r2+10*my.bili);
// my.re.left=x-r2;
// my.re.top=y-r2;
// my.re.right=x+r2;
// my.re.bottom=y+r2;
if(jineng!=0){
final float bl=2;
my.re.left=x-r2*bl;
my.re.top=y-r2*bl;
my.re.right=x+r2*bl;
my.re.bottom=y+r2*bl;
//蓝色框框未下移
g.drawBitmap(imgdown,null,my.re,p);
}
my.re.left=x-r2;
my.re.top=y-r2;
my.re.right=x+r2;
my.re.bottom=y+r2;
///////////////////////////////////////////////////////////
//技能图片和技能边框下移
my.re.top+=drawpx;
my.re.bottom+=drawpx;
g.drawBitmap(img,null,my.re,p);
my.re.left-=drawpx;
my.re.top-=drawpx;
my.re.right+=drawpx;
my.re.bottom+=drawpx;
g.drawBitmap(imgborder,null,my.re,p);
if(jineng!=0){
my.re.left=xxx-r3;
my.re.top=yyy-r3;
my.re.right=xxx+r3;
my.re.bottom=yyy+r3;
g.drawBitmap(imgyaogan,null,my.re,p);
//cancle
my.re.left= calcelx -r2;
my.re.top= cancely -r2;
my.re.right= calcelx +r2;
my.re.bottom= cancely +r2;
g.drawBitmap(imgcancel,null,my.re,p);
}
}else{
g.drawBitmap(img,null,my.re,p);
if(jineng!=0 && cd>0) {
p.setTextSize(40*my.bili);
p.setColor(Color.WHITE);
g.drawBitmap(imgcd,null,my.re,p);
float f=cd/100f;
f=(int)f;
f=f/10;
g.drawText(String.valueOf(f),x-p.getTextSize()*4/5,y+p.getTextSize()/3,p);
}
my.re.left-=drawpx;
my.re.top-=drawpx;
my.re.right+=drawpx;
my.re.bottom+=drawpx;
g.drawBitmap(imgborder,null,my.re,p);
}
}
}
这样多点触控会好写很多,刚开始我是用一个view做监听的 写到我心态爆炸。。。
package com.yaogan.liziguo.yaogan;
import android.content.Context;
import android.graphics.Color;
import android.view.MotionEvent;
import android.view.View;
/**
* Created by Liziguo on 2018/6/16.
*/
public class OnTouchSkill extends View {
/*
A 普通攻击
Q 技能1
W 技能2
E 技能3
R 没有R
*/
public Skill A,Q,W,E;
public OnTouchSkill(Context context,Skill a,Skill q,Skill w,Skill e) {
super(context);
A=a;Q=q;W=w;E=e;
setBackgroundColor(Color.WHITE);
getBackground().setAlpha(my.ontouchAlpha);//0-255
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent ev) {
final float xx = ev.getX() + getX(), yy = ev.getY() + getY();
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
if ( A.in(xx, yy)) {
A.down();
} else if ( Q.in(xx, yy)) {
Q.down();
} else if ( W.in(xx, yy)) {
W.down();
} else if ( E.in(xx, yy)) {
E.down();
}
}
if (my.skill != null) my.skill.move(xx, yy);
if(ev.getAction()==MotionEvent.ACTION_UP){
A.down = false;
Q.down = false;
W.down = false;
E.down = false;
}
return true;
}
});
}
}
package com.yaogan.liziguo.yaogan;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.widget.RelativeLayout;
/**
* Created by Liziguo on 2018/6/15.
*/
public class Hua extends RelativeLayout implements Runnable{ //继承RelativeLayout 实现Runnable接口
private Paint p;//画笔
private Move m=new Move();//移动摇杆
/*
A 普通攻击
Q 技能1
W 技能2
E 技能3
R 没有R
*/
public Skill A=new Skill(0,100, BitmapFactory.decodeResource(getResources(),R.mipmap.putonggongji)) {
@Override
public void down_main() { }
@Override
public void move_main() { }
@Override
public void up() { }
};
public Skill Q=new Skill(1,1000, BitmapFactory.decodeResource(getResources(),R.mipmap.skill1)) {
@Override
public void down_main() { }
@Override
public void move_main() { }
@Override
public void up() {
down_main=false;
if(!cancel){ //技能冷却时间
last= System.currentTimeMillis();
}
}
};
public Skill W=new Skill(2,1000, BitmapFactory.decodeResource(getResources(),R.mipmap.skill2)) {
@Override
public void down_main() { }
@Override
public void move_main() { }
@Override
public void up() {
down_main=false;
if(!cancel){
last= System.currentTimeMillis();
}
}
};
public Skill E=new Skill(3,1000, BitmapFactory.decodeResource(getResources(),R.mipmap.skill3)) {
@Override
public void down_main() { }
@Override
public void move_main() { }
@Override
public void up() {
down_main=false;
if(!cancel){
last= System.currentTimeMillis();
}
}
};
public Hua(Context context) {
super(context);
p=new Paint();
setBackgroundColor(Color.BLACK);//背景颜色设为黑色
//实例化一个OnTouchMove
OnTouchMove onTouchMove=new OnTouchMove(context,m);
//把onTouchMove添加进来 宽度为屏幕的1/3 高度为屏幕的1/2
addView(onTouchMove,my.w/3,my.h/2);
//设置onTouchMove的位置
onTouchMove.setX(0);
onTouchMove.setY(my.h/2);
//添加技能摇杆监听
OnTouchSkill onTouchSkill=new OnTouchSkill(context,A,Q,W,E);//后添加的优先级高
addView(onTouchSkill);
onTouchSkill.setX(my.w*0.7f-85*my.bili);
onTouchSkill.setY(my.h/2-85*my.bili);
new Thread(this).start();//启动重绘线程
}
@Override
protected void onDraw(Canvas g) {//重写onDraw方法
super.onDraw(g);
m.onDraw(g,p);//画移动摇杆
//画技能
A.onDraw(g,p);
Q.onDraw(g,p);
W.onDraw(g,p);
E.onDraw(g,p);
}
@Override
public void run() { //每隔20毫秒刷新一次画布
while(true){
try {Thread.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
//计算冷却时间
A.next();
Q.next();
W.next();
E.next();
//释放技能
if (my.skill != null) {
my.skill.down_main();//教程用不到该方法
my.skill.move_main();//教程用不到该方法
if (my.skill.down == false) {
my.skill.up();
my.skill = null;
}
}
postInvalidate();//重绘 在子线程重绘不能调用Invalidate()方法
}
}
}
运行下看看效果吧
public class my { //这个类当一个全局变量使用
public static int w,h;//屏幕的宽高
public static float bili;
public static MainActivity main;
public static RectF re=new RectF();
public static int ontouchAlpha=0;//把触控区透明度改成0
public static Bitmap border,cancel,down,yaogan,cd;
public static Skill skill;//当前正在使用的技能
}
再运行下
下载地址:https://download.csdn.net/download/u010756046/10482434