1. 说明
本音乐播放器基于Android开发,原为我和另外两个小伙伴在上学期间一起做的一个小项目,近来有时间整理一下。之前我有文章已经介绍了播放界面的功能实现(Android音乐播放器开发),但介绍的比较粗糙,接下来会做更细致化的整理。源码已同步到Gitee仓库,GitHub仓库,觉得还不错的话帮忙点个“star”吧,非常感谢。
服务端使用的是比较传统的servlet和jdbc传递数据,整理完之后,新版本会修改为SSM框架,更加简洁高效。安卓端使用的也都是基础的工具,比如音乐播放功能的实现也是借助于入门级的MediaPlayer类,目前关于安卓端没有什么更改的想法。
(适用于平时做个小课设的小伙伴们)
2. 登录界面设计
- 新建一个空白activity
- 分析一下需求
一般的登录界面都需要输入账户和密码,还需要发起登录申请的按钮,另外,还需要启动注册和修改密码的按钮。
具体如下图所示。
布局文件(activity_login.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/b"
android:orientation="vertical">
<!--标题栏-->
<include layout="@layout/main_title_bar"></include>
<!--显示头像,这里把头像定为了固定图像,这里完全可以为每个账户挑选各自的头像-->
<ImageView
android:id="@+id/iv_head"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_marginTop="25dp"
android:layout_gravity="center_horizontal"
android:background="@drawable/x"/>
<!--输入框-->
<EditText
android:id="@+id/et_user_name"
android:layout_width="fill_parent"
android:layout_height="48dp"
android:layout_marginTop="35dp"
android:layout_marginLeft="35dp"
android:layout_marginRight="35dp"
android:layout_gravity="center_horizontal"
android:background="@drawable/login_user_name_bg"
android:drawableLeft="@drawable/user_name_icon"
android:drawablePadding="10dp"
android:paddingLeft="8dp"
android:gravity="center_vertical"
android:hint="请输入用户名"
android:singleLine="true"
android:textColor="#000000"
android:textColorHint="#a3a3a3"
android:textSize="14sp"/>
<!--输入框-->
<EditText
android:id="@+id/et_psw"
android:layout_width="fill_parent"
android:layout_height="48dp"
android:layout_gravity="center_horizontal"
android:layout_marginLeft="35dp"
android:layout_marginRight="35dp"
android:background="@drawable/login_psw_bg"
android:drawableLeft="@drawable/psw_icon"
android:drawablePadding="10dp"
android:paddingLeft="8dp"
android:gravity="center_vertical"
android:hint="请输入密码"
android:inputType="textPassword"
android:singleLine="true"
android:textColor="#000000"
android:textColorHint="#a3a3a3"
android:textSize="14sp"/>
<!--上面inputType设置为textPassword,在输入密码时就会隐藏密码 -->
<!--按钮-->
<Button
android:id="@+id/btn_login"
android:layout_width="fill_parent"
android:layout_height="40dp"
android:layout_marginTop="15dp"
android:layout_marginLeft="35dp"
android:layout_marginRight="35dp"
android:layout_gravity="center_horizontal"
android:background="@drawable/register_selector"
android:text="登 录"
android:textColor="@android:color/white"
android:textSize="18sp"/>
<!--显示tv register , find_psw -->
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_marginTop="8dp"
android:layout_marginLeft="35dp"
android:layout_marginRight="35dp"
android:gravity="center_horizontal"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_register"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_horizontal"
android:padding="8dp"
android:text="立即注册"
android:textColor="@android:color/white"
android:textSize="14sp" />
<!--layout_weight="1" layout_width="0dp"实现均分效果-->
<TextView
android:id="@+id/tv_find_psw"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_horizontal"
android:padding="8dp"
android:text="修改密码"
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
大致界面和布局如下图所示:(左为登录界面,右为布局效果)
标题栏使用了一个单独的xml文件(main_title_bar.xml),使用时直接使用include导入即可。
<?xml version="1.0" encoding="utf-8"?>
<!--标题栏与返回键的创建,独立在main_title_bar.xml中-->
<!--标题栏设置高度为50dp,宽度为match_parent,设置背景颜色为透明 @android:color/transparent-->
<!--RelativeLayout为相对布局-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/title_bar"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@android:color/transparent">
<!--设置返回键TextView为高度50dp,宽度为50dp;id为android:id="@+id/tv_back"-->
<!--layout_alignParentLeft为与父控件左对齐-->
<!--layout_centerVertical为控件垂直居中-->
<!--标题栏界面中的返回键在按下与弹起时,返回键会有明显的区别,这种效果通过背景选择器进行实现-->
<TextView
android:id="@+id/tv_back"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:background="@drawable/go_back_selector"/>
<!--设置id为android:id="@+id/tv_main_title-->
<!--该TextView为显示文本-->
<!--layout_centerInparent为居中显示-->
<TextView
android:id="@+id/tv_main_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="20sp"
android:layout_centerInParent="true"/>
</RelativeLayout>
3. 登录界面功能实现
先贴全部代码
public class LoginActivity extends Activity {
//一些声明
private TextView mMainTitle; //主标题
private TextView mBack; //返回
private TextView mRegister; //注册
private TextView mChangePwd; //修改密码
private Button mLogin; //登录按钮
private EditText mUserName; //输入账户
private EditText mPwd; //输入密码
private String userName, pwd; //登录所需的账户和密码
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
initView(); //初始化一些控件
initEvent(); //初始化相关事件
}
private void initEvent(){
//监听返回键的点击事件
mBack.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//登录界面销毁
LoginActivity.this.finish();
}
});
//注册按钮
mRegister.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view){
//跳转到注册界面
Intent intent=new Intent(LoginActivity.this,RegisterActivity.class);
startActivity(intent);
}
});
//修改密码按钮
mChangePwd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//跳转到修改密码界面
Intent intent = new Intent(LoginActivity.this,ChangePwdActivity.class);
startActivity(intent);
}
});
//登录按钮
mLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
userName=mUserName.getText().toString().trim(); //获取用户名。.trim是为了去除字符串两侧对于空格
pwd=mPwd.getText().toString().trim(); //获取登录密码
//检测是否为空字符串
if(TextUtils.isEmpty(userName)){
Toast.makeText(LoginActivity.this, "请输入用户名", Toast.LENGTH_SHORT).show();
return;
}else if(TextUtils.isEmpty(pwd)){
Toast.makeText(LoginActivity.this, "请输入密码", Toast.LENGTH_SHORT).show();
return;
}
loginThread();
}
});
}
private void loginThread(){
//涉及到网络的请求都需要在子线程内完成
new Thread(){
public void run(){
try {
JSONObject result = RequestServlet.login(userName, pwd);
//将数据传递到主线程
Message msg = new Message();
msg.what=1;
msg.obj = result;
handler.sendMessage(msg);
}catch (Exception e){
e.printStackTrace();
}
}
}.start();
}
private Handler handler = new Handler() {
public void handleMessage(Message msg) {
if (msg.what == 1) {
JSONObject result = (JSONObject) msg.obj;
if(result == null){
Toast.makeText(LoginActivity.this, "账户错误", Toast.LENGTH_SHORT).show();
}
else{
String password = result.optString("password");
if(pwd.equals(password)){
//密码正确
Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
//传递数据
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
intent.putExtra("result",result.toString()); //把用户信息传递到播放界面
//销毁登录界面
LoginActivity.this.finish();
//跳转到主界面,登录成功的状态传递到 MainActivity 中
startActivity(intent);
return;
}
else{
Toast.makeText(LoginActivity.this, "密码错误", Toast.LENGTH_SHORT).show();
}
}
}
}
};
private void initView(){
mMainTitle = this.findViewById(R.id.tv_main_title);
mBack = this.findViewById(R.id.tv_back);
mRegister = this.findViewById(R.id.tv_register);
mChangePwd = this.findViewById(R.id.tv_change_psw);
mLogin = this.findViewById(R.id.btn_login);
mUserName = this.findViewById(R.id.et_user_name);
mPwd = this.findViewById(R.id.et_psw);
mMainTitle.setText("登录");
}
}
声明一些变量,是为了绑定界面中的控件,实现功能
private TextView mMainTitle; //主标题
private TextView mBack; //返回
private TextView mRegister; //注册
private TextView mChangePwd; //修改密码
private Button mLogin; //登录按钮
private EditText mUserName; //输入账户
private EditText mPwd; //输入密码
private String userName, pwd; //登录所需的账户和密码
加载时就需要初始化界面和一些事件
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
initView(); //初始化一些控件
initEvent(); //初始化相关事件
}
初始化界面,首先需要绑定界面中的控件,如果需要在加载界面时操作界面变化,都是在这里设置
private void initView(){
mMainTitle = this.findViewById(R.id.tv_main_title);
mBack = this.findViewById(R.id.tv_back);
mRegister = this.findViewById(R.id.tv_register);
mChangePwd = this.findViewById(R.id.tv_change_psw);
mLogin = this.findViewById(R.id.btn_login);
mUserName = this.findViewById(R.id.et_user_name);
mPwd = this.findViewById(R.id.et_psw);
mMainTitle.setText("登录");
}
初始化事件,主要是监听各个按钮,按钮被点击时做出事件响应
private void initEvent(){
//监听返回键的点击事件
mBack.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//登录界面销毁
LoginActivity.this.finish();
}
});
//注册按钮
mRegister.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view){
//跳转到注册界面
Intent intent=new Intent(LoginActivity.this,RegisterActivity.class);
startActivity(intent);
}
});
//修改密码按钮
mChangePwd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//跳转到修改密码界面
Intent intent = new Intent(LoginActivity.this,ChangePwdActivity.class);
startActivity(intent);
}
});
//登录按钮
mLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
userName=mUserName.getText().toString().trim(); //获取用户名。.trim是为了去除字符串两侧对于空格
pwd=mPwd.getText().toString().trim(); //获取登录密码
//检测是否为空字符串
if(TextUtils.isEmpty(userName)){
Toast.makeText(LoginActivity.this, "请输入用户名", Toast.LENGTH_SHORT).show();
return;
}else if(TextUtils.isEmpty(pwd)){
Toast.makeText(LoginActivity.this, "请输入密码", Toast.LENGTH_SHORT).show();
return;
}
loginThread();
}
});
}
返回按钮如果被点击,销毁当前界面,返回终端主界面。注册、修改密码按钮被点击,跳转到各自界面。登录按钮被点击,需要向数据库请求数据,因为在Android操作中,所有网络操作必须放在子线程中,所以这里单独写了一个方法。
private void loginThread(){
//涉及到网络的请求都需要在子线程内完成
new Thread(){
public void run(){
try {
JSONObject result = RequestServlet.login(userName, pwd);
//将数据传递到主线程
Message msg = new Message();
msg.what=1;
msg.obj = result;
handler.sendMessage(msg);
}catch (Exception e){
e.printStackTrace();
}
}
}.start();
}
与服务端进行交互的部分单独写了一个类RequestServlet
,稍后会做介绍。这里介绍一下Message,由于子线程不能操作UI,只有主线程可以进行UI操作,因此子线程获取到的数据需要传递到主线程,而Message就是主线程和子线程传递数据的载体,它封装了需要传递的数据。
主线程使用Handler解析封装的数据。上面调用login方法,会在服务端获取一个User对象,封装了用户信息。如果返回的用户信息为null,那么在数据库中并不存在当前账户,需要做出提示"账户错误"。如果用户信息不为空,则需要进一步对比密码信息,如果密码错误,提示错误信息,如果密码也正确,就跳转到音乐播放界面(Intent),同时将用户信息一并传递到播放界面。
private Handler handler = new Handler() {
public void handleMessage(Message msg) {
if (msg.what == 1) {
JSONObject result = (JSONObject) msg.obj;
if(result == null){
Toast.makeText(LoginActivity.this, "账户错误", Toast.LENGTH_SHORT).show();
}
else{
String password = result.optString("password");
if(pwd.equals(password)){
//密码正确
Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
//传递数据
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
intent.putExtra("result",result.toString()); //把用户信息传递到播放界面
//销毁登录界面
LoginActivity.this.finish();
//跳转到主界面,登录成功的状态传递到 MainActivity 中
startActivity(intent);
return;
}
else{
Toast.makeText(LoginActivity.this, "密码错误", Toast.LENGTH_SHORT).show();
}
}
}
}
};
RequestServlet
一个类,用于连接服务端,获取信息。
先贴出所有代码(目前只有login方法,后续完善其它功能时再做补充)。
public class RequestServlet {
private static final String LOGIN_SERVLET = "http://192.168.43.xxx:8080/musicplayer/login";
private static HttpURLConnection conn;
private static JSONObject JSONobj;
public static HttpURLConnection getConn(String path){
try {
URL url = new URL(path);
conn = (HttpURLConnection) url.openConnection();
//设置请求方式
conn.setRequestMethod("GET");
//超时时间
conn.setConnectTimeout(5000);
}catch (Exception e){
e.printStackTrace();
}
return conn;
}
private static JSONObject getJSON(String str){
try {
JSONobj = new JSONObject(str);
}catch (Exception e){
e.printStackTrace();
}
return JSONobj;
}
//解析输入流为String
public static String streamToString(InputStream is) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
//new一个StringBuffer用于字符串拼接
StringBuffer sb = new StringBuffer();
String line = null;
try {
//当输入流内容读取完毕时
while ((line = reader.readLine()) != null) {
sb.append(line);
}
//关闭流数据
is.close();
reader.close();
return sb.toString();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
//登录
public static JSONObject login(String account, String password){
JSONObject result = null;
String path = LOGIN_SERVLET+"?account="+account+"&password="+password;
HttpURLConnection conn;
try {
conn = getConn(path);
int code = conn.getResponseCode(); //http相应状态吗,200代表相应成功
if (code == 200){
InputStream stream = conn.getInputStream();
String str = streamToString(stream);
result = getJSON(str);
conn.disconnect();
}
}catch (Exception e){
e.printStackTrace();
}
return result;
}
}
在这里,LOGIN_SERVLET是连接servlet固定不变的部分,因此将其作为一个常量,后面接相应参数。
private static final String LOGIN_SERVLET = "http://192.168.43.xxx:8080/musicplayer/login";
如果使用虚拟机测试,连接servlet一般使用"http://localhost:8080"或"http://127.0.0.1:8080"。这里我使用了一个局域网做测试,需要将url修改为服务端的ip。windows系统查询本地ip的方法为:cmd–>ipconfig
在login方法中,首先需要根据url获取网络连接HttpURLConnection,根据连接获取文件流,再将文件流转为string型数据,最后转为json型数据,返回用户信息。
public static JSONObject login(String account, String password){
JSONObject result = null;
String path = LOGIN_SERVLET+"?account="+account+"&password="+password;
HttpURLConnection conn;
try {
conn = getConn(path);
int code = conn.getResponseCode(); //http相应状态吗,200代表相应成功
if (code == 200){
InputStream stream = conn.getInputStream();
String str = streamToString(stream);
result = getJSON(str);
conn.disconnect();
}
}catch (Exception e){
e.printStackTrace();
}
return result;
}
4. 测试
测试使用真机测试。环境:Android 10
在AndroidManifest.xml中将登录界面设置为软件启动界面
这里需要启动手机开发者模式,打开手机调试,连接Android studio
服务端启动Tomcat,客户端输出“cun”的账户名和密码做测试。
测试结果表明,程序运行成功。
5. 测试中遇到的问题系列
- 报错:java.io.IOException: Cleartext HTTP traffic to localhost not permitted
从Android 6.0开始引入了对Https的推荐支持,与以往不同,Android P的系统上面默认所有Http的请求都被阻止了。
解决方法:在AndroidManifest.xml中添加设置,允许使用Http请求。
android:usesCleartextTraffic="true"
- 报错:java.net.SocketException: socket failed: EACCES
没有下载文件的权限
解决方法:在AndroidManifest.xml中添加权限
<uses-permission android:name="android.permission.INTERNET" />
6.
近来发现一些小伙伴不看开头啊。
程序已经放到两个仓库了,觉得有帮助到你的话,就请给本文点个赞,给仓库点个star吧!万分感谢!