Android课设 —— App制作

之前的N讲其实只是课堂的笔记2333,期末考核就是APP制作,要求至少要8个Activity,或者是20个有效类,前后大概忙活了1个多礼拜(当然一大半的时间在google,原谅我那么菜),下面做个记录,防止以后会用到。

一、需求阶段

在 Github参考了很多,最后选择做一个提供综合性功能,用来休闲(修仙?)放松的App。(港真,不就是模块的堆砌吗2333)

  1. 图片浏览功能:每日推荐10张不同的壁纸,每张壁纸可以点开预览并进行保存。
  2. 视频播放功能:类似于抖音,能实现滑动播放;
  3. 知乎每日精选推荐功能:借用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. 项目整体架构

如图:


项目架构.JPG

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 缓缓神 (滑稽)

plmm.jpg

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,有需要评论回,溜~

你可能感兴趣的:(Android课设 —— App制作)