編輯:關於Android編程
如果你是在校大學生,或許你用多了各種課程表,比如課程格子,超級課程表。它們都有一個共同點就是可以一鍵導入教務處的課程。那麼一直都是用戶的我們,沒有考慮過它是如何實現的。那麼現在就來模仿一款”超級課程表“。
PS:由於超級課程表是商用軟件,原本提取了一些圖片,但是為了避免涉及侵權問題,所有圖片均已使用一張綠色圓圈代替,背景圖片也以顏色代替,缺乏美觀,如果你覺得太丑,可以自己尋找圖片代替。
那麼說了這麼久,先來看看這款高仿的軟件長什麼樣子。本文的代碼做過精簡,所以界面可能有出入。
好了,界面太丑,不忍直視,先暫時忽略,本文的重點不是UI,而是如何提取課程。
先做下准備工作。
HttpWatch抓包分析工具。此工具的使用後文介紹
Litepal數據持久化orm,郭大神的大作,挺好用的orm,用法詳見郭霖博客。
Async-android-http 數據異步請求框架,這裡主要用到這個框架的異步請求以及session保持的功能,或許大多數人沒有使用過這個框架的會話保持功能,反正個人覺得就是一神器,操作十分簡單,就1句話,不然用HttpClient可能就沒那麼簡單了,要自己寫好多內容。具體用法參見github
Jsoup網頁內容解析框架,可支持jquery選擇器。可以支持從本地加載html,遠程加載html,支持數據抽取,數據修改等功能,如果能靈活運用這個框架,那麼你想抓取什麼東西都不在話下。
既然要導入課程表,那麼一定要登錄教務處,結論是需要教務處的賬號密碼,這個好辦,每個學生都有賬號密碼。那麼怎麼登錄呢,這個當然不是我們人工登錄了,只要提供賬號密碼,由程序來幫我們完成登錄過程以及課程的提取過程。如果登錄?首先打開教務處登錄界面,打開HttpWatch進行跟蹤。輸入賬號,密碼,驗證碼(驗證碼視具體學校不同,有些學校不含驗證碼,有些學校含驗證碼,驗證碼的處理後文進行說明),輸入完成後點擊登錄,再點擊查看課程的菜單,之後停止HttpWatch錄制,把文件保存一下進行分析。打開保存後的文件,查看登錄時提交的參數及一些信息,記錄下來,同時記錄查看課程頁提交的參數及信息。
先看登錄頁面提交的參數,參數均是POST提交,這可以通過HttpWatch看到提交方式
__VIEWSTATE:有這個值頁面生成的,這裡我直接使用這個固定值而不去抓取,這個值是.net根據表單參數自動生成的。理論上同一個頁面是不會變動的。
Button1:傳空值即可
hidPdrs:傳空值即可
lbLanguage:傳空值即可
RadioButtonList1:圖上是亂碼,通過查看網頁源代碼可知該值是學生,因為我們是以學生的角色登錄的
TextBox2:這個值是密碼,傳密碼即可
txtSecrect:這個值是驗證碼,傳對應的驗證碼即可
txtUserName:這個值是學號,傳學號即可
你以為只要提交這些參數就好了嗎,那麼你就錯了,我們還有設置請求頭信息,如下圖
我們不必設置所有請求頭信息,只需要設置Host,Referer,User-Agent(可不設)。
請求頭設置完畢了,那麼來說一個重大的問題,就是驗證碼的問題,這裡有三種方式供選擇。
在登錄之前抓取驗證碼,顯示出來,供用戶輸入。
使用正方的bug1,為什麼是bug1呢,因為後面一種方法利用了bug2,bug1,bug2不一定所有學校適用,正方的默認登錄頁面是default2.aspx,如果這個頁面有驗證碼,你可以試試default1.aspx-default6.aspx六個頁面,運氣好的話可能會有不需要驗證碼的頁面。這時候你使用該頁面進行登錄即可(提交參數會不同,具體自己抓包分析)
使用正方的bug2,不得不說這個bug2,大概是某個程序猿在某男某月某日無意間留下的把,那麼怎麼使用這個bug呢,很簡單,登錄的時候直接傳驗證碼為空值或者空字符串過去就好了,有人說,你他媽逗我,這都行,恩,真的行。為什麼行呢,原因可能是正方後台程序沒有判斷傳過來的值是不是空。我們模擬登錄的時候並沒有去請求驗證碼的頁面,所有不會產生驗證碼(此時為空字符串或者空值)和cookie,當我們提交空驗證碼時,後台接收到的值就是空字符串,兩個空字符串做比較當然相等了,以上只是猜測,畢竟正方是.net的,.net的處理機制本人不是很清楚。
說了這麼多理論知識,來點實際的把,先完成登錄界面的代碼
很簡單,就是賬號,密碼,以及驗證碼,這裡驗證碼被我隱藏了,因為我使用了bug2,不需要請求驗證碼,對應的界面隱藏掉,但是如果你把他顯示出來,獲取驗證碼讓用戶輸入也是可以的。
在登錄之前先初始化一下cookie,這一步必須在請求之前設置。
/** * 初始化Cookie */ private void initCookie(Context context) { //必須在請求前初始化 cookie = new PersistentCookieStore(context); HttpUtil.getClient().setCookieStore(cookie); }
package cn.lizhangqu.kb.util; import org.apache.http.Header; import android.app.ProgressDialog; import android.content.Context; import android.widget.Toast; import cn.lizhangqu.kb.service.LinkService; import com.loopj.android.http.AsyncHttpClient; import com.loopj.android.http.AsyncHttpResponseHandler; import com.loopj.android.http.BinaryHttpResponseHandler; import com.loopj.android.http.RequestParams; /** * Http請求工具類 * @author lizhangqu * @date 2015-2-1 */ /** * @author Administrator * */ public class HttpUtil { private static AsyncHttpClient client = new AsyncHttpClient(); // 實例話對象 // Host地址 public static final String HOST = ***.***.***.***; // 基礎地址 public static final String URL_BASE = http://***.***.***.***/; // 驗證碼地址 public static final String URL_CODE = http://***.***.***.***/CheckCode.aspx; // 登陸地址 public static final String URL_LOGIN = http://***.***.***.***/default2.aspx; // 登錄成功的首頁 public static String URL_MAIN = http://***.***.***.***/xs_main.aspx?xh=XH; // 請求地址 public static String URL_QUERY = http://***.***.***.***/QUERY; /** * 請求參數 */ public static String Button1 = ; public static String hidPdrs = ; public static String hidsc = ; public static String lbLanguage = ; public static String RadioButtonList1 = 學生; public static String __VIEWSTATE = dDwyODE2NTM0OTg7Oz7YiHv1mHkLj1OkgkF90IvNTvBrLQ==; public static String TextBox2 = null; public static String txtSecretCode = null; public static String txtUserName = null; // 靜態初始化 static { client.setTimeout(10000); // 設置鏈接超時,如果不設置,默認為10s // 設置請求頭 client.addHeader(Host, HOST); client.addHeader(Referer, URL_LOGIN); client.addHeader(User-Agent, Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko); } /** * get,用一個完整url獲取一個string對象 * * @param urlString * @param res */ public static void get(String urlString, AsyncHttpResponseHandler res) { client.get(urlString, res); } /** * get,url裡面帶參數 * * @param urlString * @param params * @param res */ public static void get(String urlString, RequestParams params, AsyncHttpResponseHandler res) { client.get(urlString, params, res); } /** * get,下載數據使用,會返回byte數據 * * @param uString * @param bHandler */ public static void get(String uString, BinaryHttpResponseHandler bHandler) { client.get(uString, bHandler); } /** * post,不帶參數 * * @param urlString * @param res */ public static void post(String urlString, AsyncHttpResponseHandler res) { client.post(urlString, res); } /** * post,帶參數 * * @param urlString * @param params * @param res */ public static void post(String urlString, RequestParams params, AsyncHttpResponseHandler res) { client.post(urlString, params, res); } /** * post,返回二進制數據時使用,會返回byte數據 * * @param uString * @param bHandler */ public static void post(String uString, BinaryHttpResponseHandler bHandler) { client.post(uString, bHandler); } /** * 返回請求客戶端 * * @return */ public static AsyncHttpClient getClient() { return client; } /** * 獲得登錄時所需的請求參數 * * @return */ public static RequestParams getLoginRequestParams() { // 設置請求參數 RequestParams params = new RequestParams(); params.add(__VIEWSTATE, __VIEWSTATE); params.add(Button1, Button1); params.add(hidPdrs, hidPdrs); params.add(hidsc, hidsc); params.add(lbLanguage, lbLanguage); params.add(RadioButtonList1, RadioButtonList1); params.add(TextBox2, TextBox2); params.add(txtSecretCode, txtSecretCode); params.add(txtUserName, txtUserName); return params; } /** * 接口回調 * @author lizhangqu * * 2015-2-22 */ public interface QueryCallback { public String handleResult(byte[] result); } /** * 登錄後查詢信息封裝好的函數 * @param context * @param linkService * @param urlName * @param callback */ public static void getQuery(final Context context, LinkService linkService, final String urlName, final QueryCallback callback) { final ProgressDialog dialog = CommonUtil.getProcessDialog(context, 正在獲取 + urlName); dialog.show(); String link = linkService.getLinkByName(urlName); if (link != null) { HttpUtil.URL_QUERY = HttpUtil.URL_QUERY.replace(QUERY, link); } else { Toast.makeText(context, 鏈接出現錯誤, Toast.LENGTH_SHORT).show(); return; } HttpUtil.getClient().addHeader(Referer, HttpUtil.URL_MAIN); HttpUtil.getClient().setURLEncodingEnabled(true); HttpUtil.get(HttpUtil.URL_QUERY, new AsyncHttpResponseHandler() { @Override public void onSuccess(int arg0, Header[] arg1, byte[] arg2) { if (callback != null) { callback.handleResult(arg2); } Toast.makeText(context, urlName + 獲取成功!!!, Toast.LENGTH_LONG) .show(); dialog.dismiss(); } @Override public void onFailure(int arg0, Header[] arg1, byte[] arg2, Throwable arg3) { dialog.dismiss(); Toast.makeText(context, urlName + 獲取失敗!!!, Toast.LENGTH_SHORT) .show(); } }); } }
地址信息被我處理掉了,替換成對應的地址即可,都是幾個簡單的函數,其中最後一個函數做了一個封裝,代碼自己讀吧,這裡就不講了。。。。。
現在查看登錄的代碼。
/** * 登錄 */ private void login() { HttpUtil.txtUserName = username.getText().toString().trim(); HttpUtil.TextBox2 = password.getText().toString().trim(); //需要時打開驗證碼注釋 //HttpUtil.txtSecretCode = secrectCode.getText().toString().trim(); if (TextUtils.isEmpty(HttpUtil.txtUserName) || TextUtils.isEmpty(HttpUtil.TextBox2)) { Toast.makeText(getApplicationContext(), 賬號或者密碼不能為空!, Toast.LENGTH_SHORT).show(); return; } final ProgressDialog dialog =CommonUtil.getProcessDialog(LoginActivity.this,正在登錄中!!!); dialog.show(); RequestParams params = HttpUtil.getLoginRequestParams();// 獲得請求參數 HttpUtil.URL_MAIN = HttpUtil.URL_MAIN.replace(XH, HttpUtil.txtUserName);// 獲得請求地址 HttpUtil.getClient().setURLEncodingEnabled(true); HttpUtil.post(HttpUtil.URL_LOGIN, params, new AsyncHttpResponseHandler() { @Override public void onSuccess(int arg0, Header[] arg1, byte[] arg2) { try { String resultContent = new String(arg2, gb2312); if(linkService.isLogin(resultContent)!=null){ String ret = linkService.parseMenu(resultContent); Log.d(TAG, login success:+ret); Toast.makeText(getApplicationContext(), 登錄成功!!!, Toast.LENGTH_SHORT).show(); jump2Main(); }else{ Toast.makeText(getApplicationContext(),賬號或者密碼錯誤!!!, Toast.LENGTH_SHORT).show(); } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } finally { dialog.dismiss(); } } @Override public void onFailure(int arg0, Header[] arg1, byte[] arg2, Throwable arg3) { Toast.makeText(getApplicationContext(), 登錄失敗!!!!, Toast.LENGTH_SHORT).show(); dialog.dismiss(); } }); }通過抓取關鍵字,判斷是否登錄成功,登錄成功則解析菜單,對應的邏輯被我封裝在service層裡了
package cn.lizhangqu.kb.service; import java.util.List; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import org.litepal.crud.DataSupport; import cn.lizhangqu.kb.model.Course; import cn.lizhangqu.kb.model.LinkNode; /** * LinNode表的業務邏輯處理 * @author lizhangqu * @date 2015-2-1 */ public class LinkService { private static volatile LinkService linkService; private LinkService(){} public static LinkService getLinkService() { if(linkService==null){ synchronized (LinkService.class) { if(linkService==null) linkService=new LinkService(); } } return linkService; } public String getLinkByName(String name){ Listfind = DataSupport.where(title=?,name).limit(1).find(LinkNode.class); if(find.size()!=0){ return find.get(0).getLink(); }else{ return null; } } public boolean save(LinkNode linknode){ return linknode.save(); } /** * 查詢所有鏈接 * * @return */ public List findAll() { return DataSupport.findAll(LinkNode.class); } public String parseMenu(String content) { LinkNode linkNode =null; StringBuilder result = new StringBuilder(); Document doc = Jsoup.parse(content); Elements elements = doc.select(ul.nav a[target=zhuti]); for (Element element : elements) { result.append(element.html() + + element.attr(href) + ); linkNode= new LinkNode(); linkNode.setTitle(element.text()); linkNode.setLink(element.attr(href)); save(linkNode); } return result.toString(); } public String isLogin(String content){ Document doc = Jsoup.parse(content, UTF-8); Elements elements = doc.select(span#xhxm); try{ Element element=elements.get(0); return element.text(); }catch(IndexOutOfBoundsException e){ //e.printStackTrace(); } return null; } }
判斷是否登錄成功的判斷依據是看頁面上是否有某某同學,歡迎你,這段信息在id為xhxm的span裡,成功後解析菜單,因為不一定只是抓課表,也可能抓成績,各種抓,所以這裡把鏈接都記錄下來,對應頁面的源代碼我會和代碼一同上傳。
如果你要使用驗證碼,則獲取驗證碼即可,對應代碼如下,就是獲得驗證碼後顯示在界面上
/** * 獲得驗證碼 */ private void getCode() { final ProgressDialog dialog =CommonUtil.getProcessDialog(LoginActivity.this,正在獲取驗證碼); dialog.show(); HttpUtil.get(HttpUtil.URL_CODE, new AsyncHttpResponseHandler() { @Override public void onSuccess(int arg0, Header[] arg1, byte[] arg2) { InputStream is = new ByteArrayInputStream(arg2); Bitmap decodeStream = BitmapFactory.decodeStream(is); code.setImageBitmap(decodeStream); Toast.makeText(getApplicationContext(), 驗證碼獲取成功!!!,Toast.LENGTH_SHORT).show(); dialog.dismiss(); } @Override public void onFailure(int arg0, Header[] arg1, byte[] arg2, Throwable arg3) { Toast.makeText(getApplicationContext(), 驗證碼獲取失敗!!!, Toast.LENGTH_SHORT).show(); dialog.dismiss(); } }); }
package cn.lizhangqu.kb.util; /** * 首頁菜單接口 * 用於定義linknode表中的標題 * @author lizhangqu * @date 2015-2-1 */ public interface LinkUtil { public static final String ZYXXK=專業選修課; public static final String QXXGXK=全校性公選課(通識限選); public static final String SYXK=實驗選課; public static final String DJKSBM=等級考試報名; public static final String GRXX=個人信息; public static final String MMXG=密碼修改; public static final String XSGRKB=學生個人課表; public static final String XSKSCX=學生考試查詢; public static final String CJCX=成績查詢; public static final String DJKSCX=等級考試查詢; public static final String JCSYXX=教材使用信息; public static final String XSXKQKCX=學生選課情況查詢; public static final String XSBKKSCX=學生補考考試查詢; public static final String XSXXYPJ=學生信息員評價; public static final String FKJGCX=反饋結果查詢; public static final String JWGG=教務公告; public static final String BMJSKBCX=部門教師課表查詢; public static final String QXKBCX=全校課表查詢; public static final String JXRLCX=教學日歷查詢; }
接下來是文章的重點,即如何解析課表。
課表是在一張table裡的,提取table裡的內容進行解析,解析方法不止一種,我在解析過程中也嘗試了多種方法,直接看代碼把
/** * 根據網頁返回結果解析課程並保存 * * @param content * @return */ public String parseCourse(String content) { StringBuilder result = new StringBuilder(); Document doc = Jsoup.parse(content); Elements semesters = doc.select(option[selected=selected]); String[] years=semesters.get(0).text().split(-); int startYear=Integer.parseInt(years[0]); int endYear=Integer.parseInt(years[1]); int semester=Integer.parseInt(semesters.get(1).text()); Elements elements = doc.select(table#Table1); Element element = elements.get(0).child(0); //移除一些無用數據 element.child(0).remove(); element.child(0).remove(); element.child(0).child(0).remove(); element.child(4).child(0).remove(); element.child(8).child(0).remove(); int rowNum = element.childNodeSize(); int[][] map = new int[11][7]; for (int i = 0; i < rowNum - 1; i++) { Element row = element.child(i); int columnNum = row.childNodeSize() - 2; for (int j = 1; j < columnNum; j++) { Element column = row.child(j); int week = fillMap(column, map, i); //填充map,獲取周幾,第幾節至第幾節 //作用:彌補不能獲取這些數據的格式 if (column.hasAttr(rowspan)) { try { System.out.println(周+ week+ 第+ (i + 1)+ 節-第+ (i + Integer.parseInt(column.attr(rowspan))) + 節); splitCourse(column.html(), startYear,endYear,semester,week, i + 1,i + Integer.parseInt(column.attr(rowspan))); } catch (NumberFormatException e) { e.printStackTrace(); } } } } return result.toString(); } /** * 根據傳進來的課程格式轉換為對應的實體類並保存 * @param sub * @param startYear * @param endYear * @param semester * @param week * @param startSection * @param endSection * @return */ private Course storeCourseByResult(String sub,int startYear,int endYear,int semester, int week, int startSection, int endSection) { // 周二第1,2節{第4-16周} 二,1,2,4,16,null // {第2-10周|3節/周} null,null,null,2,10,3節/周 // 周二第1,2節{第4-16周|雙周} 二,1,2,4,16,雙周 // 周二第1節{第4-16周} 二,1,null,4,16,null // 周二第1節{第4-16周|雙周} 二,1,null,4,16,雙周 // str格式如上,這裡只是簡單考慮每個課都只有兩節課,實際上有三節和四節,模式就要改動,其他匹配模式請自行修改 String reg = 周?(.)?第?(\d{1,2})?,?(\d{1,2})?節?\{第(\d{1,2})-(\d{1,2})周\|?((.*周))?\}; String splitPattern = ; String[] temp = sub.split(splitPattern); Pattern pattern = Pattern.compile(reg); Matcher matcher = pattern.matcher(temp[1]); matcher.matches(); Course course = new Course(); //課程開始學年 course.setStartYear(startYear); //課程結束學年 course.setEndYear(endYear); //課程學期 course.setSemester(semester); //課程名 course.setCourseName(temp[0]); //課程時間,冗余字段 course.setCourseTime(temp[1]); //教師 course.setTeacher(temp[2]); try { // 數組可能越界,即沒有教室 course.setClasssroom(temp[3]); } catch (ArrayIndexOutOfBoundsException e) { course.setClasssroom(無教室); } //周幾,可能為空,此時使用傳進來的值 if (null != matcher.group(1)){ course.setDayOfWeek(getDayOfWeek(matcher.group(1))); }else{ course.setDayOfWeek(getDayOfWeek(week+)); } //課程開始節數,可能為空,此時使用傳進來的值 if (null != matcher.group(2)){ course.setStartSection(Integer.parseInt(matcher.group(2))); }else{ course.setStartSection(startSection); } //課程結束時的節數,可能為空,此時使用傳進來的值 if (null != matcher.group(3)){ course.setEndSection(Integer.parseInt(matcher.group(3))); }else{ course.setEndSection(endSection); } //起始周 course.setStartWeek(Integer.parseInt(matcher.group(4))); //結束周 course.setEndWeek(Integer.parseInt(matcher.group(5))); //單雙周 String t = matcher.group(6); setEveryWeekByChinese(t, course); save(course); return course; } /** * 提取課程格式,可能包含多節課 * @param str * @param startYear * @param endYear * @param semester * @param week * @param startSection * @param endSection * @return */ private int splitCourse(String str, int startYear,int endYear,int semester,int week, int startSection, int endSection) { String pattern = ; String[] split = str.split(pattern); if (split.length > 1) {// 如果大於一節課 for (int i = 0; i < split.length; i++) { if (!(split[i].startsWith( ) && split[i].endsWith( ))) { storeCourseByResult(split[i], startYear,endYear,semester,week, startSection, endSection);// 保存單節課 } else { // 文化地理(網絡課程) 周日第10節{第17-17周} 李宏偉 // 以上格式的特殊處理,此種格式在沒有教師的情況下產生,即教室留空後 依舊存在 int brLength = .length(); String substring = split[i].substring(brLength, split[i].length() - brLength); storeCourseByResult(substring, startYear,endYear,semester,week, startSection, endSection);// 保存單節課 } } return split.length; } else { storeCourseByResult(str, startYear,endYear,semester,week, startSection, endSection);// 保存 return 1; } } /** * 填充map,獲取周幾,第幾節課至第幾節課 * @param childColumn * @param map * @param i * @return 周幾 */ public static int fillMap(Element childColumn, int map[][], int i) { //這個函數的作用自行領悟,總之就是返回周幾,也是無意中發現的,於是就這樣獲取了,作用是雙重保障,因為有些課事無法根據正則匹配出周幾第幾節到第幾節 boolean hasAttr = childColumn.hasAttr(rowspan); int week = 0; if (hasAttr) { for (int t = 0; t < map[0].length; t++) { if (map[i][t] == 0) { int r = Integer.parseInt(childColumn.attr(rowspan)); for (int l = 0; l < r; l++) { map[i + l][t] = 1; } week = t + 1; break; } } } else { if (childColumn.childNodes().size() > 1) { childColumn.attr(rowspan, 1); } for (int t = 0; t < map[0].length; t++) { if (map[i][t] == 0) { map[i][t] = 1; week = t + 1; break; } } } return week; } /** * 設置單雙周 * @param week * @param course */ public void setEveryWeekByChinese(String week, Course course) { // 1代表單周,2代表雙周 if (week != null) { if (week.equals(單周)) course.setEveryWeek(1); else if (week.equals(雙周)) course.setEveryWeek(2); } // 默認值為0,代表每周 } /**根據中文數字一,二,三,四,五,六,日,轉換為對應的阿拉伯數字 * @param day * @return int */ public int getDayOfWeek(String day) { if (day.equals(一)) return 1; else if (day.equals(二)) return 2; else if (day.equals(三)) return 3; else if (day.equals(四)) return 4; else if (day.equals(五)) return 5; else if (day.equals(六)) return 6; else if (day.equals(日)) return 7; else return 0; }
課程的實體類
package cn.lizhangqu.kb.model; import org.litepal.crud.DataSupport; /** * 課程實體類 * @author lizhangqu * @date 2015-2-1 */ public class Course extends DataSupport{ private int id;//主鍵,自增 private int startYear;//學年開始年 private int endYear;//學年結束年 private int semester;//學期 private String courseName;//課程名 private String courseTime;//課程時間,冗余字段 private String classsroom;//教室 private String teacher;//老師 private int dayOfWeek;//星期幾 private int startSection;//第幾節課開始 private int endSection;//第幾節課結束 private int startWeek;//開始周 private int endWeek;//結束周 private int everyWeek;//標記是否是單雙周,0為每周,1單周,2雙周 public int getStartYear() { return startYear; } public void setStartYear(int startYear) { this.startYear = startYear; } public int getEndYear() { return endYear; } public void setEndYear(int endYear) { this.endYear = endYear; } public int getSemester() { return semester; } public void setSemester(int semester) { this.semester = semester; } public int getId() { return id; } public void setId(int id) { this.id = id; } public int getEveryWeek() { return everyWeek; } public void setEveryWeek(int everyWeek) { this.everyWeek = everyWeek; } public int getDayOfWeek() { return dayOfWeek; } public void setDayOfWeek(int dayOfWeek) { this.dayOfWeek = dayOfWeek; } public int getStartSection() { return startSection; } public void setStartSection(int startSection) { this.startSection = startSection; } public int getEndSection() { return endSection; } public void setEndSection(int endSection) { this.endSection = endSection; } public int getStartWeek() { return startWeek; } public void setStartWeek(int startWeek) { this.startWeek = startWeek; } public int getEndWeek() { return endWeek; } public void setEndWeek(int endWeek) { this.endWeek = endWeek; } public String getCourseName() { return courseName; } public void setCourseName(String courseName) { this.courseName = courseName; } public String getCourseTime() { return courseTime; } public void setCourseTime(String courseTime) { this.courseTime = courseTime; } public String getClasssroom() { return classsroom; } public void setClasssroom(String classsroom) { this.classsroom = classsroom; } public String getTeacher() { return teacher; } public void setTeacher(String teacher) { this.teacher = teacher; } @Override public String toString() { return Course [id= + id + , startYear= + startYear + , endYear= + endYear + , semester= + semester + , courseName= + courseName + , courseTime= + courseTime + , classsroom= + classsroom + , teacher= + teacher + , dayOfWeek= + dayOfWeek + , startSection= + startSection + , endSection= + endSection + , startWeek= + startWeek + , endWeek= + endWeek + , everyWeek= + everyWeek + ]; } }
以上代碼是提取課程的關鍵代碼,課程的格式是在一個table裡,tr裡有很多td,td裡就是課程,一個td裡可能不止一節課
td有rowspan屬性,代表占了幾行,2代表占了兩行,也就是兩節課,有些課不是兩節課的,rowspan的值也就對應改變,課程的節數有1,2,3,4節都有。我們可以根據課程的時間提取該課程是周幾上課,第幾節課到第幾節課,有了這些信息就可以在界面上顯示出來了,但是,有些格式,如第2-10周|3節/周是沒辦法提取時間的,這時候就用一定的技巧提取它,這裡使用了fillmap函數對一個7*12的數組進行填充,原理是掃描一行,如果具有rowspan值,則填充該行該列為1,如果rowspan大於等於2,則該列下面幾行對應的列也填充為1,等到掃描下一行的時候,該位置不會有課程,且不會有td,則如果是一個空td,則填充該行第一個為0的位置。技巧有點難以理解,具體細節稍微自己琢磨領悟下,這樣課程的信息就都提取出來了,當然提取方式不止一種。這種方法也不一定能提取所有格式的課程。
提取完畢後進行顯示,本來呢是使用LinearLayout簡單達到超級課程表的效果的,後來稍微暴力的使用了下自定義ViewGroup,注意了,這個自定義ViewGroup不具有現實使用意義,只是為了展示效果,裡面的代碼都太暴力了。。。所以看過一遍就無視吧,簡直不忍直視
首先是自定義屬性
然後是課程自定義View,繼承Button,增加一些課程信息而已。
package cn.lizhangqu.kb.view; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.widget.Button; import cn.lizhangqu.kb.R; public class CourseView extends Button { private int courseId; private int startSection; private int endSection; private int weekDay; public CourseView(Context context) { this(context,null); } public CourseView(Context context, AttributeSet attrs) { this(context, attrs,0); } public CourseView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray array=context.obtainStyledAttributes(attrs, R.styleable.CourseView); courseId = array.getInt(R.styleable.CourseView_courseId, 0); startSection=array.getInt(R.styleable.CourseView_startSection, 0); endSection=array.getInt(R.styleable.CourseView_endSection, 0); weekDay=array.getInt(R.styleable.CourseView_weekDay, 0); array.recycle(); } public int getCourseId() { return courseId; } public void setCourseId(int courseId) { this.courseId = courseId; } public int getStartSection() { return startSection; } public void setStartSection(int startSection) { this.startSection = startSection; } public int getEndSection() { return endSection; } public void setEndSection(int endSection) { this.endSection = endSection; } public int getWeek() { return weekDay; } public void setWeek(int week) { this.weekDay = week; } }最後是自定義布局,簡單暴力,注意了,這個布局沒有處理重復時間的課程,也就是說沒有處理單雙周的情況,只是用來簡單顯示
package cn.lizhangqu.kb.view; import java.util.ArrayList; import java.util.List; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; public class CourseLayout extends ViewGroup { private Listcourses = new ArrayList (); private int width;//布局寬度 private int height;//布局高度 private int sectionHeight;//每節課高度 private int sectionWidth;//每節課寬度 private int sectionNumber = 12;//一天的節數 private int dayNumber = 7;//一周的天數 private int divideWidth = 2;//分隔線寬度,dp private int divideHeight = 2;//分隔線高度,dp public CourseLayout(Context context) { this(context, null); } public CourseLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CourseLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); width = getScreenWidth();//默認寬度全屏 height = dip2px(600);//默認高度600dp divideWidth = dip2px(2);//默認分隔線寬度2dp divideHeight = dip2px(2);//默認分隔線高度2dp } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(width, height); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { courses.clear();//清除 sectionHeight = (getMeasuredHeight() - divideWidth * sectionNumber)/ sectionNumber;//計算每節課高度 sectionWidth = (getMeasuredWidth() - divideWidth * dayNumber)/ dayNumber;//計算每節課寬度 int count = getChildCount();//獲得子控件個數 for (int i = 0; i < count; i++) { CourseView child = (CourseView) getChildAt(i); courses.add(child);//增加到list中 int week = child.getWeek();//獲得周幾 int startSection = child.getStartSection();//開始節數 int endSection = child.getEndSection();//結束節數 int left = sectionWidth * (week - 1) + (week) * divideWidth;//計算左邊的坐標 int right = left + sectionWidth;//計算右邊坐標 int top = sectionHeight * (startSection - 1) + (startSection) * divideHeight;//計算頂部坐標 int bottom = top + (endSection - startSection + 1) * sectionHeight+ (endSection - startSection) * divideHeight;//計算底部坐標 child.layout(left, top, right, bottom); } } public int dip2px(float dip) { float scale = getContext().getResources().getDisplayMetrics().density; return (int) (dip * scale + 0.5f); } public int getScreenWidth() { WindowManager manager = (WindowManager) getContext().getSystemService( Context.WINDOW_SERVICE); DisplayMetrics displayMetrics = new DisplayMetrics(); manager.getDefaultDisplay().getMetrics(displayMetrics); return displayMetrics.widthPixels; } }
package cn.lizhangqu.kb.activity; import java.util.List; import android.app.Activity; import android.content.res.ColorStateList; import android.graphics.Color; import android.os.Bundle; import android.util.TypedValue; import android.view.Gravity; import cn.lizhangqu.kb.R; import cn.lizhangqu.kb.model.Course; import cn.lizhangqu.kb.service.CourseService; import cn.lizhangqu.kb.util.CommonUtil; import cn.lizhangqu.kb.view.CourseLayout; import cn.lizhangqu.kb.view.CourseView; /** * @author lizhangqu * @date 2015-2-1 */ public class CourseActivity extends Activity { //某節課的背景圖,用於隨機獲取 private int[] bg={R.drawable.kb1,R.drawable.kb2,R.drawable.kb3,R.drawable.kb4,R.drawable.kb5,R.drawable.kb6,R.drawable.kb7}; private CourseService courseService; private CourseLayout layout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_course); initValue(); initView(); } /** * 初始化變量 */ private void initValue() { courseService=CourseService.getCourseService(); } /** * 初始化視圖 */ private void initView() { //這裡有邏輯問題,只是簡單的顯示了下數據,數據並不一定是顯示在正確位置 //課程可能有重疊 //課程可能有1節課的,2節課的,3節課的,因此這裡應該改成在自定義View上顯示更合理 Listcourses=courseService.findAll();//獲得數據庫中的課程 layout=(CourseLayout) findViewById(R.id.courses); Course course=null; //循環遍歷 for (int i = 0; i < courses.size(); i++) { course=courses.get(i); CourseView view=new CourseView(getApplicationContext()); view.setCourseId(course.getId()); view.setStartSection(course.getStartSection()); view.setEndSection(course.getEndSection()); view.setWeek(course.getDayOfWeek()); int bgRes=bg[CommonUtil.getRandom(bg.length-1)];//隨機獲取背景色 view.setBackgroundResource(bgRes); view.setText(course.getCourseName()+@+course.getClasssroom()); view.setTextColor(Color.WHITE); view.setTextSize(12); view.setGravity(Gravity.CENTER); layout.addView(view); } } }
整個過程可簡單概括為抓包分析,數據提取,數據顯示,其中關鍵的一步就是數據的提取。這個過程中有個注意點就是抓課程數據的時候header請求頭信息裡的referer信息請務必設置為登錄成功後的網址,即http://***.***.***.***/xs_main.aspx?xh=XH,否則抓數據的時候頁面會被循環重定向,將抓不到數據,程序也會報異常。
本文實例講述了Android列表對話框用法。分享給大家供大家參考。具體如下:main.xml布局文件:<?xml version=1.0 encoding=
Android中的動畫主要分為三類1.Drawable Animation2.View Animation3.Property Animation這裡介紹其中的兩類,Dr
現在很多安全類的軟件,比如360手機助手,百度手機助手等等,都有一個懸浮窗,可以飄浮在桌面上,方便用戶使用一些常用的操作。今天這篇文章,就是介紹如何實現桌面懸浮窗效果的。
Android ViewGroup中的Scroller與computeScroll的有什麼關系?答:沒有直接的關系知道了答案,是不是意味著下文就沒必要看了,如果說對Vie