之前的N讲其实只是课堂的笔记2333,期末考核就是APP制作,要求至少要8个Activity,或者是20个有效类,前后大概忙活了1个多礼拜(当然一大半的时间在google,原谅我那么菜),下面做个记录,防止以后会用到。
一、需求阶段
在 Github参考了很多,最后选择做一个提供综合性功能,用来休闲(修仙?)放松的App。(港真,不就是模块的堆砌吗2333)
- 图片浏览功能:每日推荐10张不同的壁纸,每张壁纸可以点开预览并进行保存。
- 视频播放功能:类似于抖音,能实现滑动播放;
- 知乎每日精选推荐功能:借用GitHub里的知乎API,推荐文章
二、实现阶段
1. 开发工具以及依赖
AS开发,gridle版本是4.6,还有PS(2333)
若非生活所迫,谁愿意把自己弄得一身才华
要导入的依赖有:
//json
implementation 'com.alibaba:fastjson:1.2.56'
//轮播依赖
implementation 'com.youth.banner:banner:1.4.10'
//Glide框架
implementation "com.github.bumptech.glide:glide:4.6.1"
//okhttp
implementation 'com.squareup.okhttp3:okhttp:3.5.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha05'
//视频框架依赖
implementation 'com.shuyu:GSYVideoPlayer:7.0.1'
其中
Gridle框架用于图片加载;
GSYVideoPlayer用于视频播放;框架源码在这里
okhttp用于网络请求;
banner用于图片显示;
2. 项目整体架构
如图:
3. 详细设计
Activity:
(1) WelcomeActivirty:用于一开始的缓存页面;
(2) LoginActivity:通过短信验证码进行登录;、
(3) MainAcitivity:可选择的三级页面;
(4) WallpaperActicity:显示点开后的壁纸以及保存图片功能;
(5) NewsActivity:显示知乎的文章;
Fragment:
(1) JokeFragment:滑动播放的视频;
(2) NewsFragment:文章界面显示;
(3) WallpaperFragment:图片界面显示;
主要Utils:
(1) HttpUtils:由于需要多次请求网络数据,而每次都会通过一个或多个链接去请求网络数据,这样的话耦合度太高,封装起来方便解耦。
(2) ScrollCalculatorUtils:实现滑动播放;
(3) ImageLoaderUtils:继承banner框架的ImageLoader方法,重写displayImage方法,通过Gridle去加载图片;
写到这里 有点累了 看个plmm 缓缓神 (滑稽)
4. 关键代码
Activity+Layout:
(1)WelcomeActivirty:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_welcome);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
startMainActivity();
}
private void startMainActivity(){
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
Intent mainIntent = new Intent(WelcomeActivity.this,LoginActivity.class);
startActivity(mainIntent);
WelcomeActivity.this.finish();
}
};
Timer timer = new Timer();
timer.schedule(timerTask,2000);
}
Layout:
需要注意的是由于新建空的Activity是有眉头的,这里需要在AndroidManifest.xml中添加代码:
(2)LoginActivity:使用手机号和验证码进行登录;
借助MobTech的工具。用正则式来匹配手机号
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.validateNum_btn:
//手机号是否为空
if(!userName.getText().toString().trim().equals("")){
//手机号是否正确
if (checkTel(userName.getText().toString().trim())) {
SMSSDK.getVerificationCode("+86",userName.getText().toString());//获取验证码
mTimeCount.start();
}else{
Toast.makeText(LoginActivity.this, "请输入正确的手机号码", Toast.LENGTH_SHORT).show();
}
}else{
Toast.makeText(LoginActivity.this, "请输入手机号码", Toast.LENGTH_SHORT).show();
}
break;
case R.id.landing_btn:
if (!userName.getText().toString().trim().equals("")) {
if (checkTel(userName.getText().toString().trim())) {
if (!validateNum.getText().toString().trim().equals("")) {
SMSSDK.submitVerificationCode("+86",userName.getText().toString().trim(),validateNum.getText().toString().trim());//提交验证
}else{
Toast.makeText(LoginActivity.this, "请输入验证码", Toast.LENGTH_SHORT).show();
}
}else{
Toast.makeText(LoginActivity.this, "请输入正确的手机号码", Toast.LENGTH_SHORT).show();
}
}else{
Toast.makeText(LoginActivity.this, "请输入手机号码", Toast.LENGTH_SHORT).show();
}
break;
}
}
//正则匹配手机号码
public boolean checkTel(String tel){
Pattern p = Pattern.compile("^[1][3,4,5,7,8,9][0-9]{9}$");
Matcher matcher = p.matcher(tel);
return matcher.matches();
}
(3)MainActivity:
3个Fragment进行切换
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.title_menu_wallpaper:
setTabSelection(0);
break;
case R.id.title_menu_joke:
setTabSelection(1);
break;
case R.id.title_menu_news:
setTabSelection(2);
break;
}
}
//根据传入的index参数来设置选中的tab页。
private void setTabSelection(int index) {
// 每次选中之前先清楚掉上次的选中状态
clearSelection();
// 开启一个Fragment事务
FragmentTransaction transaction = fragmentManager.beginTransaction();
// 先隐藏掉所有的Fragment,以防止有多个Fragment显示在界面上的情况
hideFragments(transaction);
switch (index) {
case 0:
wallpaperLayout.setBackgroundColor(0xff00ff00);
if (wallpaperFragment == null) {
// 如果wallpaperFragment为空,则创建一个并添加到界面上
wallpaperFragment = new WallpaperFragment();
transaction.add(R.id.content,wallpaperFragment);
} else {
// 如果MessageFragment不为空,则直接将它显示出来
transaction.show(wallpaperFragment);
}
break;
case 1:
jokeLayout.setBackgroundColor(0xff00ff00);
if (jokeFragment == null) {
// 如果Fragment为空,则创建一个并添加到界面上
Log.i("joke","start");
jokeFragment = new JokeFragment();
transaction.add(R.id.content, jokeFragment);
} else {
// 如果Fragment不为空,则直接将它显示出来
transaction.show(jokeFragment);
Log.i("jokeover2","start");
}
break;
case 2:
newsLayout.setBackgroundColor(0xff00ff00);
if (newsFragment == null) {
newsFragment = new NewsFragment();
transaction.add(R.id.content, newsFragment);
Log.i("over1","start");
} else {
transaction.show(newsFragment);
Log.i("over2","start");
}
break;
}
transaction.commit();
}
//将所有的Fragment都置为隐藏状态。
private void hideFragments(FragmentTransaction transaction) {
if (wallpaperFragment != null) {
transaction.hide(wallpaperFragment);
}
if (jokeFragment != null) {
transaction.hide(jokeFragment);
jokeFragment.onPause();
}
if (newsFragment != null) {
transaction.hide(newsFragment);
}
}
// 清除掉所有的选中状态。
private void clearSelection() {
wallpaperLayout.setBackgroundColor(0x00000000);
jokeLayout.setBackgroundColor(0x00000000);
newsLayout.setBackgroundColor(0x00000000);
}
(4) WallpaperActivity:
壁纸点开后,由于需要进行保存图片功能,在现在的gridle版本中需要动态申请内存读取权限:
//动态申请权限
private final int REQUEST_EXTERNAL_STORAGE = 1;
private String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE };
public void verifyStoragePermissions(Activity activity) {
int permission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (permission != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
}
}
将传入的图片URL,通过HttpUtils工具类,初始化图片:
private void init(String wallpaperUrl){
HttpUtils httpUtils = HttpUtils.getHttpUtils();
httpUtils.getUrlAsyn(wallpaperUrl, new HttpUtils.HttpCallback() {
@Override
public void success(Call call, Response response) throws IOException {
// 获得Response对象当中的数据
InputStream inputStream = response.body().byteStream();
//将图片显示到ImageView中
bitmap = BitmapFactory.decodeStream(inputStream);
//在线程中执行UI更新操作
runOnUiThread(new Runnable() {
@Override
public void run() {
wallpaper.setImageBitmap(bitmap);
}
});
inputStream.close();
}
@Override
public void error(Call call, IOException e) {
Toast.makeText(WallpaperActivity.this,"加载失败",Toast.LENGTH_SHORT).show();
}
});
}
Fragment:
JokeFragment:
比较重要的就是视频播放的Fragment,其中的初始化数据需要放在子线程中进行:
适配器里的代码下面介绍
public void initData(){
jokeList = new ArrayList<>();
final Handler newshandler = new Handler(){
@SuppressLint("WrongConstant")
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
JSONObject data = (JSONObject) msg.obj;
JSONArray jokeDataList = data.getJSONArray("result");
for (int i = 0; i < jokeDataList.size(); i++){
JokeEntity jokeEntity = new JokeEntity();
JSONObject jokeId = jokeDataList.getJSONObject(i);
String jokeName = jokeId.getString("name");
String jokeHeader = jokeId.getString("header");
String jokeVideo = jokeId.getString("video");
jokeEntity.setJokeName(jokeName);
jokeEntity.setJokeHeader(jokeHeader);
jokeEntity.setJokeVideo(jokeVideo);
jokeList.add(jokeEntity);
}
//构造 LinearLayputManager,并设置方向。
layoutManager = new LinearLayoutManager(getActivity());
//设置滑动方向:纵向
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
//添加Android自带的分割线
jokeRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity(),DividerItemDecoration.VERTICAL));
jokeRecyclerView.setLayoutManager(layoutManager);
//设置适配器
jokeAdapter = new JokeAdapter(getActivity(), jokeList);
jokeRecyclerView.setAdapter(jokeAdapter);
jokeRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
int firstVisibleItem, lastVisibleItem;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
scrollCalculatorUtils.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
firstVisibleItem = layoutManager.findFirstVisibleItemPosition();
lastVisibleItem = layoutManager.findLastVisibleItemPosition();
//这是滑动自动播放的代码
scrollCalculatorUtils.onScroll(recyclerView, firstVisibleItem, lastVisibleItem, lastVisibleItem - firstVisibleItem);
}
});
}
};
WallpaperFragment
从URL中取数据同上,在Handle中使用HttpsUtils。由于这里用的是banner,所以initView里需要注意:
public void initView() {
ImageLoaderUtils imageLoaderUtils = new ImageLoaderUtils();
banner.setBannerStyle(BannerConfig.CIRCLE_INDICATOR);
//设置图片加载器
banner.setImageLoader(imageLoaderUtils);
//设置viewpager的默认动画
banner.setBannerAnimation(Transformer.Default);
//设置轮播图片间隔时间(单位毫秒)
banner.setDelayTime(3000);
banner.isAutoPlay(true);
//设置指示器位置
banner.setIndicatorGravity(BannerConfig.CENTER);
banner.setImages(imagePath);
banner.setOnBannerListener(this);
banner.start();
}
Adapter:
由于JokeFragment里用的是RecycleView。所以需要Adarter进行数据绑定更新;
JokeAdapter:
先需要实体类来封装视频里的所有属性,并写好get,set方法:
private String jokeName; //作者
private String jokeHeaderUrl; //头像图片
private String jokeVideoUrl;
绑定器(Holder)里:
private ImageView jokeHeader;
private TextView jokeName;
private StandardGSYVideoPlayer jokeVideo;
protected Context context = null;
public JokeHolder(Context context, View view) {
super(view);
this.context = context;
jokeHeader = view.findViewById(R.id.joke_item_header);
jokeName = view.findViewById(R.id.joke_item_author);
jokeVideo = view.findViewById(R.id.joke_item_video);
}
public void setJokeHeader(ImageView jokeHeader) {
this.jokeHeader = jokeHeader;
}
public void setJokeVideo(StandardGSYVideoPlayer jokeVideo) {
this.jokeVideo = jokeVideo;
}
public void setJokeName(TextView jokeName) {
this.jokeName = jokeName;
}
public ImageView getJokeHeader() {
return jokeHeader;
}
public StandardGSYVideoPlayer getJokeVideo() {
return jokeVideo;
}
public TextView getJokeName() {
return jokeName;
}
适配器(Adapter)里:
private List jokeEntityList = new ArrayList<>();
Context context = null;
public JokeAdapter(Context context, List list){
this.context = context;
jokeEntityList = list;
}
//创建ViewHolder
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
Log.i("begin","onCreateViewHolder");
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_joke,parent,false);
RecyclerView.ViewHolder viewHolder = new JokeHolder(context, view);
return viewHolder;
}
//绑定ViewHolder,每加载一项都会调用一次
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
Log.i("test","onBindViewHolder");
JokeHolder jokeHolder = (JokeHolder) holder;
JokeEntity jokeEntity = jokeEntityList.get(position);
jokeHolder.getJokeName().setText(jokeEntity.getJokeName());
Log.i("video",jokeEntity.getJokeHeader());
jokeHolder.getJokeVideo().setUpLazy(jokeEntity.getJokeVideo(), true,null,null,"");
//view.post方法实现在子线程中更新UI
new Thread(new Runnable() {
@Override
public void run() {
Bitmap header = BaseUtil.getBitmap(jokeEntity.getJokeHeader());
jokeHolder.getJokeHeader().post(new Runnable() {
@Override
public void run() {
jokeHolder.getJokeHeader().setImageBitmap(header);
}
});
}
}).start();
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getItemCount() {
return jokeEntityList.size();
}
工具代码:
HttpsUtils(这是网上嫖的 2333 小声BB)
private static HttpUtils httpUtils = new HttpUtils();
private OkHttpClient okHttpClient;
private static final HashMap> cookieStore = new HashMap<>();
private HttpUtils(){
//创建OkHttpClient的builder
okhttp3.OkHttpClient.Builder clientBuilder = new okhttp3.OkHttpClient.Builder();
//设置超时时间
clientBuilder.connectTimeout(10, TimeUnit.SECONDS);
okHttpClient = clientBuilder.build();
}
//单例模式(饿汉式),提前创建好封装类,为了多次调用
//使本类唯一,只能允许一处线程进入,不允许有第二处线程进入
public static HttpUtils getHttpUtils(){
if (httpUtils == null){
httpUtils = new HttpUtils();
}
return httpUtils;
}
//自定义网络回调接口
public interface HttpCallback{
//请求成功时的监听方法
void success(Call call, Response response) throws IOException;
//请求失败时的监听方法
void error(Call call, IOException e);
}
/**
* 异步get请求,子进程请求网络
* @param url
* @param httpCallback
*/
public void getUrlAsyn(String url, final HttpCallback httpCallback) {
// 创建Request对象
Request request = new Request.Builder()
.url(url)
.get()
.build();
// 创建Call对象
Call call = okHttpClient.newCall(request);
// 调用call对象的enqueue(异步)发送请求
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
httpCallback.error(call,e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
httpCallback.success(call, response);
}
});
}
DownloadImageUtils(也是网上嫖的 继续小声BB)
private static Context context;
private static String filePath;
private static Bitmap mBitmap;
private static String mSaveMessage = "失败";
private final static String TAG = "PictureActivity";
private static ProgressDialog mSaveDialog = null;
public static void donwloadImg(Context contexts, String filePaths) {
context = contexts;
filePath = filePaths;
mSaveDialog = ProgressDialog.show(context, "保存图片", "图片正在保存中,请稍等...", true);
new Thread(saveFileRunnable).start();
}
private static Runnable saveFileRunnable = new Runnable() {
@Override
public void run() {
try {
if (!TextUtils.isEmpty(filePath)) { //网络图片
// 对资源链接
URL url = new URL(filePath);
//打开输入流
InputStream inputStream = url.openStream();
//对网上资源进行下载转换位图图片
mBitmap = BitmapFactory.decodeStream(inputStream);
inputStream.close();
}
saveFile(mBitmap);
mSaveMessage = "图片保存成功!";
} catch (IOException e) {
mSaveMessage = "图片保存失败!";
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
messageHandler.sendMessage(messageHandler.obtainMessage());
}
};
private static Handler messageHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
mSaveDialog.dismiss();
Toast.makeText(context, mSaveMessage, Toast.LENGTH_SHORT).show();
}
};
//保存图片
public static void saveFile(Bitmap bm) throws IOException {
File dirFile = new File(Environment.getExternalStorageDirectory().getPath());
if (!dirFile.exists()) {
dirFile.mkdir();
}
String fileName = UUID.randomUUID().toString() + ".jpg";
File myCaptureFile = new File(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera/" + fileName);
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(myCaptureFile));
bm.compress(Bitmap.CompressFormat.JPEG, 80, bos);
bos.flush();
bos.close();
// //把图片保存后声明这个广播事件通知系统相册有新图片到来
// Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
// Uri uri = Uri.fromFile(myCaptureFile);
// intent.setData(uri);
// context.sendBroadcast(intent);
}
上面两个工具类都是网上的,查了好多资料,现在也找不到原博主网址了 2333 别给我发律师函就好!
5. 结束语:
关键的东西也就那么多,那么点东西居然写了一个礼拜,还是我太菜 2333,有需要评论回,溜~