本文参考自:打造超级课程表
一、显示课表页面的制作
①、介绍
用过的超标的同学都知道,超标的课表页面是可以滑动的,并且背景为透明色,可以任意修改背景。
效果展示:
②、制作流程
那么如何制作出这样的表格呢?
(1)、首先我们得知道,Android提供了哪些可以用来制作表格的控件。
详情参照:
如何作表格(3)——GridView+RecyclerView
(2)、我们可以从效果图上看出,整个布局分为两个部分头部和可滑动的部分。
头部的制作
首先:选择什么样控件来制作头部的格子:
大家一看发现只有一行的表格,但是第一列的大小和其他列的大小是不一样的。大家肯定马上加想到了用TableLayout和GridLayout来制作头部的表格,当然如果不嫌麻烦可以直接用TextView堆加成一行。由于GridView生成的格子大小一致,所以不可以使用。(Recycler是可以制作的,但是总感觉有点大材小用了)。
个人选择GridLayout来制作该头部。(当然小伙伴也可以使用其他的控件来进行头部的制作,看看哪个更方便)
其次:设置每个格子的高度,和宽度。由于第一行比较特殊还需要分离出来。
本人是获取屏幕大小,然后分成15分,第一行占1份,其他行占2分来确定。
最后:分割线的确定
本人选择了,使用<View>的方式来添加了竖直分割线,并利用drawable中的<shape>来设置边框。(当然这不是最简便,和最有效的方法)
最后的效果:
添加头部:
<GridLayout
android:id="@+id/main_grid_title"
android:layout_width="match_parent"
android:layout_height="@dimen/table_row_height" //设置表格的高度为50
android:background="@drawable/table_frame" //背景
android:rowCount="1"
android:columnCount="15">
</GridLayout>columCount = "15" :其中有7列用来填充分割线的(这种做法,分割线和表格内容没有分开,容易混乱,不建议使用。本人懒癌发作了,请见谅。
背景:
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/transparent"/> //中间背景色为透明。
<stroke
android:color="@color/blue"
android:width="1dp"/>
</shape>
利用代码填充表格内容:
//表格的内容
private static final String [] TITLE_DATA = {"9月","周一","周二","周三","周四","周五","周六","周日"};
private GridLayout mGlClsTitle;
//屏幕宽度的1/15
private int mTableDistance;
protected void onCreateView(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);
mGlClsTitle = getViewById(R.id.main_grid_title);
mTableDistance = getScreenPixelWidth()/15;
}
//设置表格的头部
private void setUpClsTitle(){
for (int i=0; i<TITLE_DATA.length; ++i){
String content = TITLE_DATA[i];
//设置LayoutParams
GridLayout.LayoutParams params = new GridLayout.LayoutParams();
//第一列的时候
if (i == 0){
params.width = mTableDistance;//宽度为总宽度的1/15
}
else {
//添加分割线
View divider = getLayoutInflater().inflate(R.layout.grid_title_form,mGlClsTitle,false);
mGlClsTitle.addView(divider);
params.width = mTableDistance * 2;
}
params.height = GridLayout.LayoutParams.MATCH_PARENT;
TextView textView = new TextView(this);
textView.setTextColor(getResources().getColor(R.color.blue));
textView.setText(content);
textView.setGravity(Gravity.CENTER);
mGlClsTitle.addView(textView,params);
}
}
显示课程内容表格的制作:
首先:选择使用哪种控件来制作表格:
我们可以知道,有些课程需要合并多行、多列来显示课程。所以毫无疑问只能使用GridLayout来制作表格。
其次:设置每行的高度和宽度,并添加课程的节数,及其背景边框
然后:使用ScrollView添加滑动效果
效果:
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scrollbars="none">
<GridLayout
android:id="@+id/main_grid_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:rowCount="13"
android:columnCount="8">
</GridLayout>
</ScrollView>
private static final int GRID_ROW_COUNT = 12;
private static final int GRID_COL_COUNT = 8; <pre code_snippet_id="1909809" snippet_file_name="blog_20161001_5_6741136" name="code" class="java" style="font-size: 18px;">
//初始化课表显示的格子
private void setUpClsContent(){
//设置每行第几节课的提示
for(int i=0; i<GRID_ROW_COUNT+1; ++i){
int row = i;
int col = 0;
GridLayout.LayoutParams params = new GridLayout.LayoutParams(
GridLayout.spec(row),GridLayout.spec(col)
);
params.width = mTableDistance;
if (i == 0){
params.height = 0;//第一行不显示
}
else {
params.height = (int) getResources().getDimension(R.dimen.table_row_height);
}
TextView textView = new TextView(this);
textView.setTextColor(getResources().getColor(R.color.blue));
textView.setText(i+"");
textView.setGravity(Gravity.CENTER);
textView.setBackground(getResources().getDrawable(R.drawable.table_frame));
mGlClsContent.addView(textView,params);
}
<span style="font-weight: normal;">//初始化表格的距离
for (int i=1; i<GRID_COL_COUNT; ++i){
int row = 0;
int col = i;
GridLayout.LayoutParams params = new GridLayout.LayoutParams(
GridLayout.spec(row),GridLayout.spec(col)
);
params.width = mTableDistance*2;
params.height = (int) getResources().getDimension(R.dimen.table_row_height);
View view = new View(this);
mGlClsContent.addView(view,params);
}</span>
<span style="font-weight: normal;">//初始化表格的距离
for (int i=1; i<GRID_COL_COUNT; ++i){
int row = 0;
int col = i;
GridLayout.LayoutParams params = new GridLayout.LayoutParams(
GridLayout.spec(row),GridLayout.spec(col)
);
params.width = mTableDistance*2;
params.height = (int) getResources().getDimension(R.dimen.table_row_height);
View view = new View(this);
mGlClsContent.addView(view,params);
}</span>(3)、模拟显示数据
首先:创建Course类
其次:获取背景颜色素材并设置背景四角弯曲效果
素材:
<color name="blue">#378BE0</color>
<color name="white">#fff</color>
<color name="transparent">#00000000</color>
<color name="light_blue">#9960BFE5</color>
<color name="light_green">#9968CA5E</color>
<color name="light_pink">#99F49C97</color>
<color name="hole_blue">#9993AAE2</color>
然后:自定义Course的内容
最后:将Course添加到新建的表格中去
public class Course {
//星期几:周一到周日
private int day;
//第几节课:总共12节
private int clsNum;
//每节课的长度
private int clsCount;
//随机的颜色
private int color;
//课程名
private String clsName;
public int getClsNum() {
return clsNum;
}
public void setClsNum(int clsNum) {
this.clsNum = clsNum;
}
public String getClsName() {
return clsName;
}
public void setClsName(String clsName) {
this.clsName = clsName;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public int getDay() {
return day;
}
public void setDay(int day) {
this.day = day;
}
public int getClsCount() {
return clsCount;
}
public void setClsCount(int clsCount) {
this.clsCount = clsCount;
}
@Override
public String toString() {
return "StuClass{" +
"clsCount=" + clsCount +
", day=" + day +
", clsNum=" + clsNum +
", color=" + color +
", clsName='" + clsName + '\'' +
'}';
}
}
其次:
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:radius="8dp"/> //弯曲四个角
</shape>
private void showCls(){
for (int i = 0; i< mStuCourseList.size(); ++i){
Course course = mStuCourseList.get(i);
int row = course.getClsNum();
int col = course.getDay();
int size = course.getClsCount();
//设定View在表格的哪行那列
GridLayout.LayoutParams params = new GridLayout.LayoutParams(
GridLayout.spec(row,size),
GridLayout.spec(col)
);
//设置View的宽高
params.width = mTableDistance*2;
params.height = (int) getResources().getDimension(R.dimen.table_row_height) * size;
params.setGravity(Gravity.FILL);
//通过代码改变<Shape>的背景颜色
GradientDrawable drawable = (GradientDrawable) getResources().getDrawable(R.drawable.cls_bg);
drawable.setColor(getResources().getColor(course.getColor()));
//设置View
TextView textView = new TextView(this);
textView.setTextColor(getResources().getColor(R.color.white));
textView.setText(course.getClsName());
textView.setGravity(Gravity.CENTER);
textView.setBackground(drawable);
//添加到表格中
mGlClsContent.addView(textView,params);
}
}
二、从正方教育系统提取课表数据
效果图:
①、登陆教育系统,观察需要提交的数据
第一点:正方使用的是Session,而不是Cookie。(关与Cookie与Session的差别请Google)说明当你登陆正方的任意一个网站的时候,网站就会返回一个Cookie,而不是像知乎一样当你登陆成功之后,返回一个Cookie给你。(这是个大坑,要注意)
(刚登陆的时候,就得到了一个Cookie。所以正方系统是无法实现持久化登陆的)
第二点:正方系统需要使用验证码登陆(网上说有绕过验证码的方法,但是没一个能成功),所以需要首先获取验证码,然后进行登陆,获取POST请求的地址。对于网络交互本文使用OkHttp请求
关于如何使用浏览器抓包,与OkHttp请求的使用请移步:
OkHttp实现模拟登陆知乎
获取验证码的地址及上传参数:
获取登陆请求的地址及上传参数:
RadioButtonList1:可能显示值为unable to decode value。该内容可以从网站的源码中找到(单击右键,查看网络源代码)
最后查询出来的结果是"学生"
②、模拟登陆学校官网
(1)、创建登陆页面
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp">
<EditText
android:id="@+id/login_et_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:hint="请输入账号"/>
<EditText
android:id="@+id/login_et_pwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:hint="请输入密码"
android:inputType="textPassword"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/login_et_codes"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginRight="20dp"
android:hint="请输入验证码"/>
<ImageView
android:id="@+id/login_iv_codes_img"
android:layout_width="90dp"
android:layout_height="40dp"
android:scaleType="fitXY"/>
</LinearLayout>
<Button
android:id="@+id/login_btn_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:text="@string/login"
android:textColor="@color/white"/>
</LinearLayout>
(2)、初始化封装一层OkHttp请求
public class HttpConnection {
private static final long TIME_OUT = 10000;
private OkHttpClient mClient;
private static HttpConnection sConnection;
private final Map<String,List<Cookie>> mCookieMap = new HashMap<>();
private HttpConnection(){
mClient = new OkHttpClient.Builder()
.connectTimeout(TIME_OUT, TimeUnit.MILLISECONDS)
.readTimeout(TIME_OUT,TimeUnit.MILLISECONDS)
.cookieJar(new MyCookieJar())
.build();
}
public static HttpConnection getInstance(){
if (sConnection == null){
sConnection = new HttpConnection();
}
return sConnection;
}
/*存储Cookie的类*/
/**
* 由于网站不能够持久化登陆,就直接将Cookie值放在HashMap中了
*/
class MyCookieJar implements CookieJar{
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
mCookieMap.put(url.host(),cookies);
}
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
List<Cookie> cookieList = mCookieMap.get(url.host());
return cookieList == null ? new ArrayList<Cookie>() : cookieList;
}
}
public void saveCookie(HttpUrl url,List<Cookie> cookies){
//如果url相同则替换
mClient.cookieJar().saveFromResponse(url,cookies);
}
public List<Cookie> getCookies(HttpUrl url){
//未判断Http是否合法
return mClient.cookieJar().loadForRequest(url);
}
/*默认使用异步加载*/
public void connectUrl(Request request, Callback callback){
Call call = mClient.newCall(request);
call.enqueue(callback);
}
public interface HttpCallBack <T>{
void callback(T data);
}
}
(3)、创建URLManager,管理网址
/**
* Created by PC on 2016/9/25.
* 存储需要用到的网址
*/
public class URLManager {
//登陆的首页
public static final String URL_BASE = "http://202.115.80.153";
//登陆的验证码
public static final String URL_CODES = "http://202.115.80.153/CheckCode.aspx?";
//登陆的判定提交地址
public static final String URL_LOGIN = "http://202.115.80.153/default2.aspx";
//登陆之后的跳转页面
public static final String URL_REFERER = "http://202.115.80.153/xs_main.aspx?xh=XH";
//课程表
public static final String URL_CLS = "http://202.115.80.153/xskbcx.aspx?xh=XH";
}
(4)、创建LoginService,封装登陆的服务,并创建回调接口,将回调设置在主线程中
逻辑:首先获取验证码,显示在ImageView上,然后在输入用户名密码进行登陆
/**
* Created by PC on 2016/9/25.
* 登陆到学校的教务网
* 需要设置为单例模式
*/
public class LoginService {
public static final String TAG = "LoginService";
private final Handler mHandler = new Handler(Looper.getMainLooper());
private Context mContext;
//设置登陆的Post参数
private String rudioButton = "学生";
private String state = "dDwyODE2NTM0OTg7Oz7QBx05W486R++11e1KrLTLz5ET2Q==";
private String button1 = "";
private String language = "";
private String hidPdrs = "";
private String hidsc = "";
private HttpConnection mConnection = HttpConnection.getInstance();
public LoginService(Context context){
mContext = context;
}
//获取验证码
/**
* 获取验证码的时候,会返回给客户端一个Cookie
* 作用:获取验证码
* @param httpCallBack
*/
public <T extends Bitmap> void getCodesImg(final HttpConnection.HttpCallBack<T> httpCallBack) throws IOException{
URL codeUrl = new URL(URLManager.URL_CODES);
final Request request = new Request.Builder()
.url(https://blog.csdn.net/codeUrl)
.build();
Callback callback = new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.d(TAG,"出错");
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()){
//用流优化当收到的数据比较大
byte [] bytes = response.body().bytes();
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
final Bitmap bitmap = BitmapFactory.decodeStream(bais);
//切换线程环境
changeEnvironment(httpCallBack,bitmap);
response.close();
}
}
};
mConnection.connectUrl(request,callback);
}
public <T> void login(final String username, String pwd, final String codes, final HttpConnection.HttpCallBack<T> httpCallBack) throws IOException{
URL url = new URL(URLManager.URL_LOGIN);
FormBody formBody = new FormBody.Builder()
.add("__VIEWSTATE",state)
.add("txtUserName",username)
.add("TextBox2",pwd)
.add("txtSecretCode",codes)
.add("RadioButtonList1",rudioButton)
.add("Button1",button1)
.add("lbLanguage",language)
.add("hidPdrs",hidPdrs)
.add("hidsc",hidsc)
.build();
final Request request = new Request.Builder()
./service/https://blog.csdn.net/url(url)
.post(formBody)
.build();
Callback callback = new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.d(TAG,"错误");
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()){
boolean isLogin = false;
//判断是否登陆成功 注释①
if(CourseParse.
parseIsLoginSucceed(response.body().string())){
isLogin = true;
}
else {
//显示登陆失败。
isLogin = false;
}
changeEnvironment(httpCallBack,isLogin);
response.close();
}
}
};
mConnection.connectUrl(request,callback);
}
private <T> void changeEnvironment(final HttpConnection.HttpCallBack httpCallBack, final T data){
mHandler.post(new Runnable() {
@Override
public void run() {
httpCallBack.callback(data);
}
});
}
}
注:如何判断是否登陆成功:
如果登陆失败,返回的Html文档,为登陆页面,
如果登陆成功后,返回的Html文档中,有某某同学,欢迎你,这段信息在id为xhxm的span里,成功后解析菜单。
(解析工具使用Jsoup)
(5)、创建CourseParase解析Html文本(是否登陆成功)
public static boolean parseIsLoginSucceed(String data){
Document doc = Jsoup.parse(data);
//查看登陆文档。
Element element = doc.getElementById("xhxm");
if (element != null){
return true;
}
return false;
}
(6)、这个时候我们就相当于进入到了学生的信息页面( http://xxx.xxx.xx.xxx/xs_main.aspx?xh="你的学号")
之后,我们点击获取进入课表的链接地址(通过抓包查询):
发现是Get请求。然后创建CourseService类
public class CourseService {
public static final String TAG = "CourseService";
private final Handler mHandler = new Handler();
private HttpConnection mConnection = HttpConnection.getInstance();
public <T extends List<Course>> void getCourse(String username, final HttpConnection.HttpCallBack<T> callBack) throws IOException {
//因为xh为学号,需要自己设置
String urlCls = URLManager.URL_CLS.replace("XH",username);
//重点:因为该网站实现的是动态跳转(有没有发现跳转到课表页面,但是网址没有变化)
//必须添加Referer到Http头,Referer指的是当前页面的网址,否则会拒绝访问
String referer = URLManager.URL_REFERER.replace("XH",username);
Log.d(TAG,urlCls);
URL url = new URL(urlCls);
Request.Builder builder = new Request.Builder();
builder.addHeader("Referer",referer);
builder./service/https://blog.csdn.net/url(url);
final Request request = builder.build();
Callback callback = new Callback() {
@Override
public void onFailure(Call call, IOException e) {
//网络较差环境下的问题
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
//登陆到课表,并解析课程表的资源
if (response.isSuccessful()){
//其实这里可以使用InputStream进行优化的。以后再说吧
final String clsDocument = response.body().string();
mHandler.post(new Runnable() {
@Override
public void run() {
T stuClassList = (T) CourseParse.parsePersonal(clsDocument);
callBack.callback(stuClassList);
}
});
response.close();
}
}
};
mConnection.connectUrl(request,callback);
}
}
(7)解析课表数据,并转化为Course(在CourseParase类中)
这里的课表数据解析情况,根据各位的课表情况而定= =,如果照搬可能出现问题
/*解析个人课表*/
public static List<Course> parsePersonal(String data){
List<Course> courses = new ArrayList<>();
Document doc = Jsoup.parse(data);
//首先获取Table
Element table = doc.getElementById("Table1");
//然后获取table中的td节点
Elements trs = table.select("tr");
//移除不需要的参数,这里表示移除前两个数值。
trs.remove(0);
trs.remove(0);
//遍历td节点
for (int i=0; i<trs.size(); ++i){
Element tr = trs.get(i);
//获取tr下的td节点,要求
Elements tds = tr.select("td[align]");
//遍历td节点
for(int j=0; j<tds.size(); ++j){
Element td = tds.get(j);
String str = td.text();
//如果数值为空则不计算。
if (str.length() != 1){
//解析文本数据
str = parsePersonalCourse(str);
Course course = new Course();
course.setClsName(str);
course.setDay(j+1);
course.setClsCount(Integer.valueOf(td.attr("rowspan")));
course.setClsNum(i+1);
Random random = new Random();
int num = random.nextInt(COLOR.length);
course.setColor(COLOR[num]);
courses.add(course);
}
}
}
return courses;
}
private static String parsePersonalCourse(String text){
//正则表达式获取课名,和教室
Pattern courseNamePattern = Pattern.compile("^.+?(\\s{1})");
Matcher courseNameMatcher = courseNamePattern.matcher(text);
courseNameMatcher.find();
String str = courseNameMatcher.group(0);
Pattern courseLocPattern = Pattern.compile("\\s{1}(\\d+)");
Matcher courseLocMatcher = courseLocPattern.matcher(text);
courseLocMatcher.find();
String data = courseLocMatcher.group(0);
return str+"@"+data;
}好了,到这里,超级课程表就出炉了~~~~~吼吼
本文介绍了如何使用Android开发制作显示课表的页面,包括使用GridLayout制作头部和内容表格,以及模拟显示数据。此外,还讨论了从正方教育系统提取课表数据的初步步骤,如登陆系统并观察所需提交的数据。
7668

被折叠的 条评论
为什么被折叠?



