我的头条
一、实验目的与内容:
目的:
掌握安卓中活动的编写、自定义用户界面的开发、能使用HTTP协议访问网络;并能通过自学能适当完善该APP界面,并使界面尽量美观。
内容要求:
-
请尽量模拟如下APP界面的功能,参考:
https://play.google.com/store/apps/details?id=mark.h.my_news_app&hl=en_US
-
该实现的界面应至少包含3个菜单,分别展示个人3个方面的信息,菜单之间要表现出一定的差异性;每个菜单可以包含2-5个条目,每个条目能响应个人某方面的偏好信息;此外,如果是响应网页,需同时体现出a) 采用浏览器浏览 与 b) 下载到本地 两种技术方案。
-
尽量多的应用参考书《第一行代码 Android》第二版第2章(活动)、第3章(UI开发)与第9章(网络技术)的各个知识点。
注意:
- 实验报告中需要有功能的描述、实验结果的截屏图像及详细说明;
- 该实验报告的所需的部分内容需要自学(如第9章);
- 也欢迎采用其它章节的知识点完成本次实验报告,如果实现的功能言之合理,会考虑酌情加分。
二、实验过程和代码与结果
1.“我的头条”APP的构建过程及结果
本实验共包含四个java文件接下来按照顺序进行介绍:
①MainActivity.java
package com.example.todaytest;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.navigation.NavigationView;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import org.json.JSONArray;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private final List<Fragment> fragmentList = new ArrayList<>();
private final String[] titles = {"Recommend", "Science", "Sport", "Entertainment", "Fashion"};
public volatile List<WebList> webNameList = new ArrayList<>();
private DrawerLayout mDrawerLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
toolbar.setTitle("");
setSupportActionBar(toolbar);
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
NavigationView navView = (NavigationView) findViewById(R.id.nav_view);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
}
navView.setCheckedItem(R.id.nav_call);
navView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(MenuItem item) {
mDrawerLayout.closeDrawers();
return true;
}
});
new Thread(new Runnable() {
@Override
public void run() {
try {
Document doc = Jsoup.connect("http://v.juhe.cn/toutiao/index?type=&key=f771f2aa48bd48c7de10e40ab22d9228").userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36").ignoreContentType(true).get();
String data_js = doc.text();
JSONObject jobj = new JSONObject(data_js).getJSONObject("result");
JSONArray jsonArray = jobj.getJSONArray("data");
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
String title = jsonObject.getString("title");
String url = jsonObject.getString("url");
byte[] bytes;
String img_url = jsonObject.getString("thumbnail_pic_s");
URL url1 = new URL(img_url);
HttpURLConnection connection = (HttpURLConnection) url1.openConnection();
connection.setConnectTimeout(5000);
connection.setRequestMethod("GET");
int code = connection.getResponseCode();
if (code == 200) {
InputStream is = connection.getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, len);
}
bytes = byteArrayOutputStream.toByteArray();
WebList webList = new WebList(title, url, bytes);
webNameList.add(webList);
}
}
} catch (Exception e) {
e.printStackTrace();
// System.out.println("error occurred!");
}
}
}).start();
initView();
}
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.toolbar, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
mDrawerLayout.openDrawer(GravityCompat.START);
break;
case R.id.settings:
Toast.makeText(this, "You clicked refresh", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
try {
Document doc = Jsoup.connect("http://v.juhe.cn/toutiao/index?type=&key=f771f2aa48bd48c7de10e40ab22d9228").userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36").ignoreContentType(true).get();
String data_js = doc.text();
JSONObject jobj = new JSONObject(data_js).getJSONObject("result");
JSONArray jsonArray = jobj.getJSONArray("data");
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
String title = jsonObject.getString("title");
String url = jsonObject.getString("url");
byte[] bytes;
String img_url = jsonObject.getString("thumbnail_pic_s");
URL url1 = new URL(img_url);
HttpURLConnection connection = (HttpURLConnection) url1.openConnection();
connection.setConnectTimeout(5000);
connection.setRequestMethod("GET");
int code = connection.getResponseCode();
if (code == 200) {
InputStream is = connection.getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, len);
}
bytes = byteArrayOutputStream.toByteArray();
WebList webList = new WebList(title, url, bytes);
webNameList.add(webList);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
break;
default:
}
return true;
}
private void initView() {
ViewPager2 viewPager2 = findViewById(R.id.view_pager2);
TabLayout tabLayout = findViewById(R.id.tab_layout);
fragmentList.add(WebListFragment.newInstance("Recommend"));
fragmentList.add(WebListFragment.newInstance("Science"));
fragmentList.add(WebListFragment.newInstance("Sport"));
fragmentList.add(WebListFragment.newInstance("Entertainment"));
fragmentList.add(WebListFragment.newInstance("Fashion"));
viewPager2.setOffscreenPageLimit(ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT);
viewPager2.setAdapter(new FragmentStateAdapter(this) {
@NonNull
@Override
public Fragment createFragment(int position) {
return fragmentList.get(position);
}
@Override
public int getItemCount() {
return fragmentList.size();
}
});
TabLayoutMediator tabLayoutMediator = new TabLayoutMediator(tabLayout, viewPager2, false, new TabLayoutMediator.TabConfigurationStrategy() { // 设置tablayout和viewpager2联动
@Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
tab.setText(titles[position]);
}
});
tabLayoutMediator.attach();
}
}
- 1~34行完成了包名以及对应类的引用,此处不做详细解释。
- 第35行到38行定义对应的参数,此处使用了两个List容器,用来存储对应的fragment内容和网页上的每条新闻内容。
- 第41行到64行重写onCreate函数,定义左侧的actionBar滑出菜单。添加监听事件,当检测到鼠标点击时展示actionBar,在actionBar开启状态下点击其他位置则关闭actionBar。
- 第66到104行利用线程去从Json里获取数据进行新闻的展示。首先,通过json获取对应API的数据。解析返回数据结构后,建立对应JSONObject,再通过JSONArray提取对应返回的“data”数据并将每一条新闻分开。通过利用for循环遍历jsonArray分离每一条新闻。对于每一条新闻,通过jsonObject的getString方法,获取对应的标题以及跳转链接。为了防止相应时间过长而造成个别新闻相应时间过长而死循环,此处也设置了相应的Timeout判断时间,即超过这个时间便认定为超时,进入下一个新闻的读取。在完成每个新闻的读取后,创建对应的新闻类后,将对应新闻放入webNameList容器中。此时,一条新闻的读取已经完成,通过循环遍历jsonArray完成对所有新闻的读取。
- 第106到109行通过onCreateOptionsMenu来创建菜单,创建左侧滑出的菜单。
- 第112行到162行定义了刷新功能,如果检测到点击了刷新按钮,则弹出“You clicked refresh”的提示并重新获取新闻,获取新闻的方式与前面的方式一样,此处不重复介绍。
- 第164行到174行定义初始化页面的方法,并增加对应的fragment以完成tab的切换。
- 第176到第188行实现对应的fragment适配器以刷新页面数据,此处重写了createFragment与getItemCount两个方法
- 第189到196行完成设置tablayout和viewpager2联动
②WebListFragment.java
package com.example.todaytest;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.json.JSONArray;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
public class WebListFragment extends Fragment {
private static final String KEY = "key";
private static String val;
private final Object lock = new Object();
public volatile List<WebList> webNameList = new ArrayList<>();
public WebListFragment() {
}
public static WebListFragment newInstance(String ty) {
val = ty;
WebListFragment fragment = new WebListFragment();
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public synchronized View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fg_weblist, container, false);
RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
MainActivity activity = (MainActivity) getActivity();
assert activity != null;
webNameList = activity.webNameList;
LinearLayoutManager layoutManager = new LinearLayoutManager(view.getContext());
recyclerView.setLayoutManager(layoutManager);
WebAdapter adapter = new WebAdapter(webNameList, view.getContext());
recyclerView.setAdapter(adapter);
return view;
}
private void init_view_data() {
new Thread(new Runnable() {
@Override
public void run() {
try {
Log.d(val, "run: init_view_data");
System.out.println(val);
Document doc = Jsoup.connect("http://v.juhe.cn/toutiao/index?type=&key=f771f2aa48bd48c7de10e40ab22d9228").userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36").ignoreContentType(true).get();
String data_js = doc.text();
JSONObject jobj = new JSONObject(data_js).getJSONObject("result");
JSONArray jsonArray = jobj.getJSONArray("data");
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
String title = jsonObject.getString("title");
String url = jsonObject.getString("url");
byte[] bytes;
String img_url = jsonObject.getString("thumbnail_pic_s");
URL url1 = new URL(img_url);
HttpURLConnection connection = (HttpURLConnection) url1.openConnection();
connection.setConnectTimeout(5000);
connection.setRequestMethod("GET");
int code = connection.getResponseCode();
if (code == 200) {
InputStream is = connection.getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, len);
}
bytes = byteArrayOutputStream.toByteArray();
WebList webList = new WebList(title, url, bytes);
webNameList.add(webList);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.notify();
}
}
}).start();
}
class WebAdapter extends RecyclerView.Adapter<WebAdapter.ViewHolder> {
private final List<WebList> mWebListList;
private final Context context;
public WebAdapter(List<WebList> webListList, Context context) {
this.context = context;
mWebListList = webListList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.weblist_item, parent, false);
final WebAdapter.ViewHolder holder = new ViewHolder(view);
holder.webView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
WebList webList = mWebListList.get(position);
Intent intent = new Intent(context, WebContentActivity.class);
intent.putExtra("webname", webList.getUrl());
startActivity(intent);
}
});
holder.webImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
WebList webList = mWebListList.get(position);
Toast.makeText(v.getContext(), "you clicked time", Toast.LENGTH_SHORT).show();
}
});
return holder;
}
@Override
public void onBindViewHolder(WebAdapter.ViewHolder holder, int position) {
WebList webList = mWebListList.get(position);
Bitmap bitmap = BitmapFactory.decodeByteArray(webList.getBit_image(), 0, webList.getBit_image().length);
holder.webImage.setImageBitmap(bitmap);
holder.webTitle.setText(webList.getNewsTitle());
}
@Override
public int getItemCount() {
return mWebListList.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
View webView;
ImageView webImage;
TextView webTitle;
public ViewHolder(View view) {
super(view);
webView = view;
webImage = (ImageView) view.findViewById(R.id.web_image);
webTitle = (TextView) view.findViewById(R.id.web_title);
}
}
}
}
- 1~31行完成了包名以及对应类的引用,此处不做详细解释。
- 第33到第40行完成对应参数定义。
- 第42行到第46行定义新建fragment的方法,并通过从MainActivity调用此函数来创建新的tab完成分类新闻的显示。
- 第54到66行为fragment创建视图,其中LayoutInflater inflater:作用类似于findViewById,findViewById用来寻找xml布局下的具体的控件(Button、TextView等),LayoutInflater inflater用来找res/layout/下的xml布局文件。ViewGroup container:表示容器,View放在里面。Bundle savedInstanceState:保存当前的状态,在活动的生命周期中,只要离开了可见阶段,活动很可能就会被进程终止,这种机制能保存当时的状态。
- 第68到第109行定义初始化视图数据方法init_view_data与MainActivity中基本一样,此处不做赘述。
- 第111行到第118行定义WebAdapter网络适配器,并实现对应的构造方法
- 第122到第145行通过RecyclerView的ViewHolder复用,一个在大小有限的窗口内展示大量数据集。当监听到鼠标点击时,通过Intent传值并展示对应数据。
- 第146到第152行通过onBindViewHolder滚动创建新视图,便于新闻的显示和展示。通过这种方式可以提高显示效率并降低内存占用。
- 第154到第158行定义函数来获得WebListList的大小
- 第159到第170行通过定义ViewHolder来提升listview滚动时的性能。ViewHolder通常出现在适配器里,为的是listview滚动的时候快速设置值,而不必每次都重新创建很多对象,从而提升性能。 在android开发中Listview是一个很重要的组件,它以列表的形式根据数据的长自适应展示具体内容,用户可以自由的定义listview每一列的布局,但当listview有大量的数据需要加载的时候,会占据大量内存,影响性能,这时候就需要按需填充并重新使用view来减少对象的创建。
③WebContentActivity.java
package com.example.todaytest;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.appcompat.app.AppCompatActivity;
public class WebContentActivity extends AppCompatActivity {
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.fg_content);
Intent intent = getIntent();
String webname = intent.getStringExtra("webname");
WebView webView = (WebView) findViewById(R.id.web_view);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient());
webView.loadUrl(webname);
}
}
- 1~10行完成了包名以及对应类的引用,此处不做详细解释。
- 第14到23行通过构建WebView来显示新闻对应网页对应的新闻,并完成对应页面的跳转。此处使用了Intent来传递数据。当用户点击新闻时会实现页面跳转。
④WebList.java
package com.example.todaytest;
public class WebList {
private String newsTitle;
private String url;
private byte[] bit_image;
public WebList(String newsTitle, String url, byte[] bit_image) {
this.newsTitle = newsTitle;
this.url = url;
this.bit_image = bit_image;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getNewsTitle() {
return newsTitle;
}
public void setNewsTitle(String newsTitle) {
this.newsTitle = newsTitle;
}
public byte[] getBit_image() {
return bit_image;
}
}
在这个文件中实现的对WebList类的定义,用来存储每一条新闻。其中,newsTitle用来存储新闻的标题,url用来存储新闻的跳转链接,bit_image用来存储新闻对应图片。并在这个类中定义了对应的构造方法以及get和set方法,此处体现了面向对象编程。
2. 请详细说明“我的头条”APP的功能、出现的关键问题及解决方案
(1)功能展示:
①开启APP后可以看到如下界面,可以看到,几条新闻排列在下方。左侧展示了新闻的图片,右侧展示了对应新闻的标题。上侧显示了本人的学号和名字。右上以及左上角都含有两个按钮,可以进行页面切换以及刷新新闻。
②点击任一新闻,即可进入对应的新闻页面查看该新闻的详细信息。退出该页面后将回到本新闻APP上次浏览到的位置。
③点击右上角类似冒号的图标可以出现“Refresh”刷新按钮,点击后新闻将重新获取并刷新。
④也可通过点击tab不同的栏目完成新闻分类的切换。
⑤通过点击左上角类似三道杠的图标可以呼出个人菜单,在后续的开发中,可以在此链接数据库完成用户与服务器的交互以提供更多用户的个性化设置。
(2)常见问题——“API的解析”
由于在实现新闻APP的过程中需要从API接口获取数据,需要使用GET请求获取API的数据。此处,要额外留意API返回数据结构。如需要解析至每条新闻并列的形式,若不为并列形式,或新闻与其他数据仍存在嵌套结构则会导致解析错误。此时Java虚拟机会抛出异常。
而在debug模式下,可以清楚看到有返回的数据,而且API接口提供的调用数据统计也显API接口被成功调用了一次,此时,即发生API解析问题,应认真核对API返回数据的结构。
若以上图为例,可以看到发回来的新闻数据存放于data中,而data存放于result中,故在进行API解析时需从外至内依次解析。先解析result再解析data。即可成功解析API。获取对应的数据只需解析对应API参数即可。
三、实验总结
(此处写你的过程,比如遇到的错误,以及解决方法,你的所想、所得)
1、添加断点进行debug
在Android Studio中添加断点进行debug的方法与其他环境类似。但基于JetBrains平台的优越性,在进行debug时,可以在变量旁清晰的看到每个变量的值。具体debug步骤与其他语言一样。
2、报错Binary XML file line #23: Binary XML file line #2: Error inflating class LinerLayout(来源课程QQ群)
经查阅资料发现大致有以下四种原因:
-
引用类名问题:
自定义了一个View,将他用于布局文件中,假设他的包名叫MyPackage,类名叫MyTestView,这个时候你在XML作为布局元素来布局的话,必须使用完整路径名,也就是包名加类名来引用,用MyPackage.MyTestView来进行引用。 -
构造函数问题:
自定义一个View,必须派生实现基类View的三个构造函数
View(Context context)
View(Context context, AttributeSet attrs)
View(Context context, AttributeSet attrs, int defStyle)
从开发者文档上的介绍来看,第二个和第三个构造函数对于XML这种引用方式是必须实现的,这三个构造函数的功能是在不同的应用场合来实例化一个View对象。 -
编译的中间文件未清理干净:
当在原生系统代码的编译环境下编译APK之后,特别是修改了XML,出现标题所述现象,这个时候你只需要删除out目录下编译生成的中间文件夹即可(在编译过程中,系统会将那个位置打印出来,建议通过路径进行查找,…/out/…/…/classes.dex,循着这个路径往前推到应用的project名字那一层文件夹),删除再重新gradle就OK了。 -
找不到资源文件:
找到对应资源文件并放入对应的正确位置即可。
3、关于卸载重装AndroidStudio
由于AndroidStudio部分情况下因部分版本不能与对应的Gradle版本匹配,此时必须删除AndroidStudio重新下载,在删除AndroidStudio后经常发生对应的Gradle并未卸载(干净)的情况,此时,建议使用搜索的方式,在C盘和AndroidStudio安装的硬盘搜索“Android”字段。并删除对应文件后。重新去官网下载最新的AndroidStudio版本。在安装好AndroidStudio后,将自动下载对应的Gradle文件以及相应组件。