前言
前面的博客裏面分別講到了自定義ViewGroup(側滑菜單)、自定義Dialog(登陸對話框以及背景透明進度條)、使用PopupWindow(下拉方式選擇年份),那麼今天要講的是自繪課程表界面,屬於android繪圖知識,在這之前我也寫過課程表界面,不過當時使用的GridView+Adapter來製作課程表,不過效果不是很好,鏈接如下:
早就準備重寫了,今天就直接重寫了整個佈局,這次是採用繪圖的方式繪製出來的,先看下效果:
代碼修改
在上一篇使用PopupWindow的代碼中,我是直接將創建PopupWinddow的代碼寫在ScoreActivity裏面:
package cn.karent.nanhang.activity;
import android.app.Activity;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.PopupWindow;
import android.widget.TextView;
import cn.karent.nanhang.R;
import cn.karent.nanhang.adapter.ScoreAdapter;
import cn.karent.nanhang.adapter.WeekAdapter;
import cn.karent.nanhang.model.Score;
import cn.karent.nanhang.util.ScreenUtil;
/**
* Created by wan on 2016/12/26.
* 顯示成績的Activity
*/
public class ScoreActivity extends Activity implements View.OnClickListener{
/**
* 顯示成績的列表
*/
private ListView mScoreList;
/**
* 分數的Adapter
*/
private ScoreAdapter mScoreAdapter;
/**
* 選擇星期的View
*/
private View mCheckWeek;
/**
* 彈出框
*/
private PopupWindow mCheckWeekPopupWindow;
/**
* PopupWindow的寬度
*/
private int mPopupWidth;
/**
* 選擇年份的ListView
*/
private ListView mList;
/**
* 用來接收所有的事件但不消費,用於消除PopupWindow無法消失的情況
*/
private View mCancelPopup;
/**
* 顯示當前的年份
*/
private TextView mCurrentYear;
/**
* 顯示週數的Adapter
*/
private WeekAdapter mWeekAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.score_layout);
mScoreList = (ListView)findViewById(R.id.score_list);
mScoreAdapter = new ScoreAdapter(this, R.layout.score_item_layout);
fillContent();
mScoreList.setAdapter(mScoreAdapter);
mCheckWeek = findViewById(R.id.score_checkWeek);
mCheckWeek.setOnClickListener(this);
mPopupWidth = ScreenUtil.dp2px(250);
mCancelPopup = findViewById(R.id.score_cancelPopup);
mCancelPopup.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if( mCheckWeekPopupWindow != null ) {
mCheckWeekPopupWindow.dismiss();
mCheckWeekPopupWindow = null;
}
return false;
}
});
mCurrentYear = (TextView)findViewById(R.id.score_week);
}
/**
* 填充測試數據
*/
private void fillContent() {
Score s = new Score();
s.setName("美食與文化");
s.setCourseTime("(總學時:20小時)");
s.setCourseProperty("全校任選課");
s.setProperty("選修");
s.setWay("考查");
s.setScore(93);
s.setCredit(1);
mScoreAdapter.add(s);
mScoreAdapter.add(s);
mScoreAdapter.add(s);
mScoreAdapter.add(s);
mScoreAdapter.add(s);
mScoreAdapter.add(s);
mScoreAdapter.add(s);
}
/**
* 初始化popupWindow當中的數據
*/
private void initPopupWindow() {
if( mWeekAdapter != null) {
mList.setAdapter(mWeekAdapter);
return;
}
mWeekAdapter = new WeekAdapter(this, R.layout.check_week_layout);
mWeekAdapter.setWeekTextView(mCurrentYear);
int year = 2015;
for(int i = 26; i > 0; i--) {
String s = year + "-" + (year + 1);
if( i % 2 == 0) {
s += "(上學期)";
} else {
s += "(下學期)";
}
mWeekAdapter.add(s);
year -= 1;
}
mList.setAdapter(mWeekAdapter);
}
/**
* 彈出選擇週數的框
* @param v
*/
@Override
public void onClick(View v) {
if( mCheckWeekPopupWindow == null ) {
mCheckWeekPopupWindow = new PopupWindow(this);
View contentView = LayoutInflater.from(this).inflate(R.layout.checkweek_popup_layout, null);
mCheckWeekPopupWindow.setContentView(contentView);
mCheckWeekPopupWindow.setWidth(mPopupWidth);
mCheckWeekPopupWindow.setHeight(ViewGroup.LayoutParams.MATCH_PARENT);
mList = (ListView) contentView.findViewById(R.id.checkweek_list);
mCheckWeekPopupWindow.setBackgroundDrawable(null);
mCheckWeekPopupWindow.setFocusable(false);
mCheckWeekPopupWindow.setOutsideTouchable(true);
mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
mCurrentYear.setText(((TextView)view).getText());
//讓popupWindow消失
mCheckWeekPopupWindow.dismiss();
mCheckWeekPopupWindow = null;
}
});
//去除背景
initPopupWindow();
//計算要偏移的像素
int offsetX = (ScreenUtil.getScreenWidth() - mPopupWidth) / 2;
mCheckWeekPopupWindow.showAsDropDown(mCheckWeek, offsetX, 0);
}
}
}
因爲今天這個課程表也需要PopupWindow,而且樣式是一樣的,我就將它提取出來了,放在一個類裏面:
package cn.karent.nanhang.util;
import android.app.Activity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.PopupWindow;
import cn.karent.nanhang.R;
/**
* Created by wan on 2016/12/28.
* 創建一個PopupWindow的
*/
public class PopupWindowUtil {
/**
* 創建PopupWindow
* @param activity 要創建PopupWindow的Activity
* @param width popupWindow的寬度
* @param l listView的監聽器
* @return 一個創建好了的PopupWindow
*/
public static PopupWindow createPopupWindow(Activity activity, int width, BaseAdapter adapter, final ItemClickListener l) {
PopupWindow checkWeekPopupWindow = new PopupWindow(activity);
View contentView = LayoutInflater.from(activity).inflate(R.layout.checkweek_popup_layout, null);
checkWeekPopupWindow.setContentView(contentView);
checkWeekPopupWindow.setWidth(width);
checkWeekPopupWindow.setHeight(ViewGroup.LayoutParams.MATCH_PARENT);
ListView list = (ListView) contentView.findViewById(R.id.checkweek_list);
//設置Adapter
list.setAdapter(adapter);
checkWeekPopupWindow.setBackgroundDrawable(null);
checkWeekPopupWindow.setFocusable(false);
checkWeekPopupWindow.setOutsideTouchable(true);
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
l.onItemClick(parent, view, position, id);
}
});
return checkWeekPopupWindow;
}
/**
* 裏面的列表項的監聽器
*/
public static interface ItemClickListener {
void onItemClick(AdapterView<?> parent, View view, int position, long id);
}
}
然後原先的ScoreActivity裏面的代碼修改爲如下:
package cn.karent.nanhang.activity;
import android.app.Activity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.PopupWindow;
import android.widget.TextView;
import cn.karent.nanhang.R;
import cn.karent.nanhang.UI.ProgressDialog;
import cn.karent.nanhang.adapter.ScoreAdapter;
import cn.karent.nanhang.adapter.WeekAdapter;
import cn.karent.nanhang.model.Score;
import cn.karent.nanhang.util.PopupWindowUtil;
import cn.karent.nanhang.util.ScreenUtil;
/**
* Created by wan on 2016/12/26.
* 顯示成績的Activity
*/
public class ScoreActivity extends Activity implements View.OnClickListener{
/**
* 顯示成績的列表
*/
private ListView mScoreList;
/**
* 分數的Adapter
*/
private ScoreAdapter mScoreAdapter;
/**
* 選擇星期的View
*/
private View mCheckWeek;
/**
* 彈出框
*/
private PopupWindow mCheckWeekPopupWindow;
/**
* PopupWindow的寬度
*/
private int mPopupWidth;
/**
* 選擇年份的ListView
*/
private ListView mList;
/**
* 用來接收所有的事件但不消費,用於消除PopupWindow無法消失的情況
*/
private View mCancelPopup;
/**
* 顯示當前的年份
*/
private TextView mCurrentYear;
/**
* 顯示週數的Adapter
*/
private WeekAdapter mWeekAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.score_layout);
mScoreList = (ListView)findViewById(R.id.score_list);
mScoreAdapter = new ScoreAdapter(this, R.layout.score_item_layout);
fillContent();
mScoreList.setAdapter(mScoreAdapter);
mCheckWeek = findViewById(R.id.score_checkWeek);
mCheckWeek.setOnClickListener(this);
mPopupWidth = ScreenUtil.dp2px(250);
mCancelPopup = findViewById(R.id.score_cancelPopup);
mCancelPopup.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if( mCheckWeekPopupWindow != null ) {
mCheckWeekPopupWindow.dismiss();
mCheckWeekPopupWindow = null;
}
return false;
}
});
mCurrentYear = (TextView)findViewById(R.id.score_week);
//顯示加載對話框
new ProgressDialog.Builder(this).create().show();
}
/**
* 填充測試數據
*/
private void fillContent() {
Score s = new Score();
s.setName("美食與文化");
s.setCourseTime("(總學時:20小時)");
s.setCourseProperty("全校任選課");
s.setProperty("選修");
s.setWay("考查");
s.setScore(93);
s.setCredit(1);
mScoreAdapter.add(s);
mScoreAdapter.add(s);
mScoreAdapter.add(s);
mScoreAdapter.add(s);
mScoreAdapter.add(s);
mScoreAdapter.add(s);
mScoreAdapter.add(s);
}
/**
* 初始化popupWindow當中的數據
*/
private void initPopupWindow() {
if( mWeekAdapter != null) {
return;
}
mWeekAdapter = new WeekAdapter(this, R.layout.check_week_layout);
mWeekAdapter.setWeekTextView(mCurrentYear);
int year = 2015;
for(int i = 26; i > 0; i--) {
String s = year + "-" + (year + 1);
if( i % 2 == 0) {
s += "(上學期)";
} else {
s += "(下學期)";
}
mWeekAdapter.add(s);
year -= 1;
}
}
/**
* 彈出選擇週數的框
* @param v
*/
@Override
public void onClick(View v) {
if( mCheckWeekPopupWindow == null ) {
initPopupWindow();
mCheckWeekPopupWindow = PopupWindowUtil.createPopupWindow(this, mPopupWidth, mWeekAdapter, new PopupWindowUtil.ItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
mCurrentYear.setText(((TextView)view).getText());
//讓popupWindow消失
mCheckWeekPopupWindow.dismiss();
mCheckWeekPopupWindow = null;
}
});
//計算要偏移的像素
int offsetX = (ScreenUtil.getScreenWidth() - mPopupWidth) / 2;
mCheckWeekPopupWindow.showAsDropDown(mCheckWeek, offsetX, 0);
}
}
}
修改就這麼多了,接下來進入今天的正題:
課程表的源碼及分析
首先整個課程表也是有佈局的,我上一張草圖:
整個界面是一個垂直的LinearLayout佈局,裏面放着四個子View,
- 使用include標籤加載的佈局
- 普通的LinearLayout
- 自定義View,自繪週數
- 自定義View,自繪課程表
下面開始上佈局文件了:
<?xml version="1.0" encoding="utf-8"?>
<!--顯示課表的佈局文件-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true">
<!--頂部返回欄-->
<include layout="@layout/back_layout"/>
<!--顯示當前年份並選擇週數-->
<LinearLayout
android:background="@android:color/white"
android:layout_width="match_parent"
android:layout_height="45dp">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center"
android:textColor="#666666"
android:textSize="18sp"
android:text="2016年"/>
<View
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_width="1px"
android:layout_height="match_parent"
android:background="@color/scoreDivideColor"/>
<RelativeLayout
android:id="@+id/course_relative_week"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/course_week"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="第18周"
android:textColor="@android:color/black"
android:textSize="18sp"
android:layout_centerInParent="true"/>
<ImageView
android:layout_marginLeft="5dp"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_centerVertical="true"
android:layout_toRightOf="@id/course_week"
android:src="@drawable/xiala"/>
</RelativeLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@drawable/back_line"/>
<!--星期幾-->
<cn.karent.nanhang.UI.CourseDateUI
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"/>
<View android:layout_width="match_parent"
android:layout_height="8dp"
android:background="@drawable/banner_above"/>
<!--接下來就是自定義圖形繪製整個課表-->
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
<cn.karent.nanhang.UI.CourseUI
android:id="@+id/course_detail"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</ScrollView>
</LinearLayout>
<!--不消費事件,只用來取消PopupWindow-->
<View
android:id="@+id/course_cancelPopup"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"/>
</RelativeLayout>
上面就是整個課程表的界面佈局了,至於爲什麼要加個額外的corse_cancelPopup我前面的博客有講,我就不贅述了,在這個佈局裏面能夠看到兩個自定義View,分別是:
cn.karent.nanhang.UI.CourseDateUI
和
cn.karent.nanhang.UI.CourseUI
這就是對應着前面的3和4了,先看自繪星期幾的源碼:
package cn.karent.nanhang.UI;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import cn.karent.nanhang.R;
import cn.karent.nanhang.util.ScreenUtil;
/**
* Created by wan on 2016/12/28.
* 繪製當前的天數
*/
public class CourseDateUI extends View {
/**
* 當前的View的偏移位置
*/
private int mOffsetX;
/**
* 當前View的寬度
*/
private int mWidth;
/**
* 當前View的高度
*/
private int mHeight;
/**
* 今天是星期幾,便於繪製背景圖,默認星期一,從0開始
*/
private int mCurrentDate = 3;
/**
* 畫筆
*/
private Paint mPaint;
/**
* 每個格子所佔的大小
*/
private int mPerWidth;
/**
* 是否是第一次加載
*/
private boolean mLoadonce = true;
/**
* 文字的大小
*/
private int mTextSize;
/**
* 要畫的日期
*/
private String[] mDates = new String[] {
"一", "二", "三", "四", "五", "六", "日"
};
/**
* 圈出當前星期幾的背景圖片
*/
private Bitmap mCurrentDateBg;
/**
* mCurrentDateBg對象所佔的矩形
*/
private Rect mSourceRect;
/**
* 文字的Y軸偏移距離
*/
private int mTextOffsetY;
/**
* 背景的y軸偏移距離
*/
private int mDateBgOffsetY;
/**
* 正常日期的顏色
*/
private int mNormalColor = Color.rgb(66, 66, 66);
/**
* 週末的顏色
*/
private int mWeekendColor = Color.rgb(255, 0, 0);
public CourseDateUI(Context context, AttributeSet attrs) {
super(context, attrs);
mHeight = ScreenUtil.dp2px(45);
mOffsetX = ScreenUtil.dp2px(20);
//畫筆初始化
mPaint = new Paint();
//反鋸齒
mPaint.setAntiAlias(true);
mTextSize = ScreenUtil.dp2px(15);
mPaint.setTextSize(mTextSize);
mCurrentDateBg = BitmapFactory.decodeResource(context.getResources(), R.drawable.week_point_icon);
mSourceRect = new Rect(0, 0, mCurrentDateBg.getWidth(), mCurrentDateBg.getHeight());
mTextOffsetY = ScreenUtil.dp2px(3);
mDateBgOffsetY = ScreenUtil.dp2px(4);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if( mLoadonce ) {
mWidth = MeasureSpec.getSize(widthMeasureSpec);
mPerWidth = (int)((mWidth - mOffsetX) * 1.0f / 7 + 0.5f);
mLoadonce = false;
}
setMeasuredDimension(mWidth, mHeight);
}
@Override
protected void onDraw(Canvas canvas) {
//繪製文字開始
String s = null;
//獲得字體的基準線
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
for( int i = 0; i < 7; i++) {
s = mDates[i];
//測量文字的寬度
float strWidth = mPaint.measureText(s);
int x = mOffsetX + i * mPerWidth ;
//因爲文字是基於基準線繪製的,所以座標不應該嚴格的按照getHeight() / 2繪製,這樣還是不會在中心
float y = getHeight() / 2 + (Math.abs(fontMetrics.ascent) - fontMetrics.descent) / 2 + mTextOffsetY;
//繪製當前是星期幾
if( mCurrentDate == i) {
Rect r1 = new Rect(x, mDateBgOffsetY, x + mPerWidth, mHeight);
canvas.drawBitmap(mCurrentDateBg, mSourceRect, r1, mPaint);
}
//星期六和星期天應該用紅色繪製
if( i == 5 || i == 6) {
mPaint.setColor(mWeekendColor);
} else {
mPaint.setColor(mNormalColor);
}
x += (mPerWidth - strWidth) / 2;
canvas.drawText(s, x, y, mPaint);
}
}
public void setCurrentDate(int currentDate) {
this.mCurrentDate = currentDate;
}
public int getCurrentDate() {
return this.mCurrentDate;
}
}
裏面的註釋已經很清楚了,只要重寫onDraw()方法然後計算座標分別繪製數字和背景,這裏面有一點一定要注意 使用Paint.measureText一定要先設調用Paint.setTextSize(),不然你後面設置了之後就無法使用前面測量的數據了,因爲畫筆的大小改了,自然畫出來的字體大小也改了,只要注意了這點就沒什麼了,剩餘的都是計算的事,接下來介紹最難的自繪課程表,先上源碼:
package cn.karent.nanhang.UI;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import cn.karent.nanhang.R;
import cn.karent.nanhang.model.CourseItem;
import cn.karent.nanhang.util.ScreenUtil;
/**
* Created by wan on 2016/12/28.
* 自定義課程的繪製界面
*/
public class CourseUI extends View{
/**
* 背景圖片
*/
private Bitmap mBackground;
/**
* 畫筆
*/
private Paint mPaint;
/**
* 每個格子的高度
*/
private int mPerHeight;
/**
* 文字的顏色
*/
private int mTextColor = Color.rgb(66, 66, 66);
/**
* 旁邊的寬度
*/
private int mSideWidth;
/**
* 當前自定義View的寬度
*/
private int mWidth;
private int mTextSize;
private Rect mBitmapRect;
/**
* 每一個表格的寬度
*/
private int mPerWidth;
/**
* 是否是第一次加載
*/
private boolean mLoadonce = true;
/**
* 每節課的信息
*/
private CourseItem[][] mChildren;
/**
* 根據課程信息生成的界面ui
*/
private Set<CourseItemUI> mChildrenUI = new HashSet<>();
/**
* 行和列
*/
private int mRow, mColumn;
/**
* 顏色
*/
private int[] mColors = new int[] {
Color.rgb(0xf6, 0x94, 0xa0), Color.rgb(0xfe, 0xa1, 0x64), Color.rgb(0xc6, 0x9e, 0xe4),
Color.rgb(0x3e, 0xd3, 0xad), Color.rgb(0xc4, 0xd8, 0x45), Color.rgb(0xf8, 0x94, 0xa0),
Color.rgb(0xa2, 0x53, 0x5c)
};
public CourseUI(Context context) {
super(context);
}
public CourseUI(Context context, AttributeSet attrs) {
super(context, attrs);
mBackground = BitmapFactory.decodeResource(context.getResources(), R.drawable.kec_back);
mSideWidth = ScreenUtil.dp2px(20);
mPerHeight = ScreenUtil.dp2px(45);
mTextSize = ScreenUtil.dp2px(13);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setTextSize(mTextSize);
mBitmapRect = new Rect(0, 0, mBackground.getWidth(), mBackground.getHeight());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int height = mPerHeight * 12;
if( mLoadonce ) {
mWidth = MeasureSpec.getSize(widthMeasureSpec);
mPerWidth = (mWidth - mSideWidth) / 7;
initChildrenUI();
mLoadonce = false;
}
setMeasuredDimension(mWidth, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawBackgroundAndNet(canvas);
drawSideIndicate(canvas);
drawChildren(canvas);
}
/**
* 繪製背景和網格線
* @param canvas
*/
private void drawBackgroundAndNet(Canvas canvas) {
Rect r = new Rect(0, 0, mWidth, mPerHeight * 12);
mPaint.setColor(Color.rgb(0xcc, 0xd8, 0xd8));
canvas.drawBitmap(mBackground, mBitmapRect, r, mPaint);
int y = mPerHeight * 12;
int x = mSideWidth;
//繪製豎線
for(int i = 0; i < 8; i++) {
x = mSideWidth + i * mPerWidth;
canvas.drawLine(x, 0, x, y, mPaint);
}
x = mWidth;
//畫豎線
for(int i = 0; i < 13; i++) {
y = i * mPerHeight;
canvas.drawLine(mSideWidth, y, x, y, mPaint);
}
}
/**
* 繪製裏面的課程表詳細信息
* @param canvas
*/
private void drawChildren(Canvas canvas) {
Iterator<CourseItemUI> iter = mChildrenUI.iterator();
while( iter.hasNext() ) {
iter.next().draw(canvas);
}
}
/**
* 繪製旁邊的第幾節課
* @param canvs
*/
private void drawSideIndicate(Canvas canvs) {
mPaint.setColor(Color.rgb(0x85, 0x90, 0x90));
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
//繪製文字開始
for(int i = 1; i <= 12; i++) {
//計算中心線
float strWidth = mPaint.measureText(i + "");
float x = (mSideWidth - strWidth) / 2;
float y = (i - 1) * mPerHeight + mPerHeight / 2 + Math.abs(fontMetrics.ascent - fontMetrics.descent) / 2;
canvs.drawText(i + "", x, y, mPaint);
}
}
/**
* 設置課程表信息
* @param courseItems 所有課程
* @param row 行數
* @param column 列數
*/
public void setChildren(CourseItem[][] courseItems, int row, int column) {
mChildren = courseItems;
mRow = row;
mColumn = column;
}
/**
* 初始化CourseItemUI數據
*/
private void initChildrenUI() {
CourseItemUI courseItemUI = null;
//將CourseItem轉換爲CourseItemUI
for(int j = 0; j < mColumn; j++) {
for(int i = 0; i < mRow; i++) {
CourseItem c = mChildren[i][j];
if( i % 2 == 0 && c != null) {
courseItemUI = new CourseItemUI();
int x = mSideWidth + j * mPerWidth;
int y = i * mPerHeight;
courseItemUI.setmX(x);
courseItemUI.setmY(y);
courseItemUI.setmWidth(mPerWidth);
courseItemUI.setmCourse(c);
courseItemUI.setBackColor(mColors[j]);
} else {
if( i % 2 == 0 && c == null) {
//說明沒有課
} else {
if( c != null ) {
if( courseItemUI != null) {
courseItemUI.setmHeight(2 * mPerHeight);
mChildrenUI.add(courseItemUI);
}
courseItemUI = null;
} else {
if( courseItemUI != null) {
courseItemUI.setmHeight(mPerHeight);
mChildrenUI.add(courseItemUI);
}
courseItemUI = null;
}
}
}
}
}
}
}
嚴格來說繪製這個也不難,既然是繪製,當然要重寫onDraw()方法了,然後依次繪製背景和網格線、左邊的第幾節課、然後就是繪製每一節課的詳細信息了,既然每一節課都有詳細信息,那麼應該讓它自己來繪製自己,而我們要做的就是將它所處的座標傳遞給它,那就先看下它的源碼吧:
package cn.karent.nanhang.UI;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.Log;
import cn.karent.nanhang.model.CourseItem;
import cn.karent.nanhang.util.ScreenUtil;
import cn.karent.nanhang.util.TextUtil;
/**
* Created by wan on 2016/12/29.
* 每一個Item,負責繪製出自身
*/
public class CourseItemUI {
/**
* 圓角矩形所佔的大小
*/
private RectF mSelfBound;
private CourseItem mCourse;
/**
* 座標與寬高
*/
private int mX;
private int mY;
private int mWidth;
private int mHeight;
/**
* 背景顏色
*/
private int mBackColor;
private Paint mPaint;
/**
* 圓角矩形的圓角半徑
*/
private int mRadius;
/**
* 圓角矩形離整個邊框的x和y距離
*/
private int mDistanceX, mDistanceY;
/**
* 字體顏色
*/
private int mTextColor = Color.rgb(0xff, 0xfd, 0xfc);
/**
* 字體的大小
*/
private int mTextSize;
/**
* 是否只有一節課
*/
private boolean mSingle = false;
public CourseItemUI() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mDistanceX = ScreenUtil.dp2px(2);
mDistanceY = ScreenUtil.dp2px(3);
mRadius = ScreenUtil.dp2px(3);
mTextSize = ScreenUtil.dp2px(12);
}
/**
* 繪製函數,這裏面將會繪製出自身,是一個圓角矩形
* @param canvas
*/
public void draw(Canvas canvas) {
drawBackground(canvas);
drawText(canvas);
}
/**
* 繪製背景,圓角矩形
* @param canvas
*/
private void drawBackground(Canvas canvas) {
mPaint.setColor(mBackColor);
//填充風格
mPaint.setStyle(Paint.Style.FILL);
if( mSelfBound == null)
initSelfBound();
canvas.drawRoundRect(mSelfBound, mRadius, mRadius, mPaint);
}
/**
* 初始化圓角矩形
*/
private void initSelfBound() {
mSelfBound = new RectF();
mSelfBound.left = mX + mDistanceX;
mSelfBound.right = mX + mWidth - mDistanceX;
mSelfBound.top = mY + mDistanceY;
mSelfBound.bottom = mY + mHeight - mDistanceY;
}
/**
* 繪製文字
* @param canvas
*/
private void drawText(Canvas canvas) {
//一箇中文佔兩個字節,也就是說一行繪製6個字節
String s = mCourse.getName();
if( s != null) {
//開始繪製
mPaint.setColor(mTextColor);
//測量之前必須先設置字體的大小,否則測量的數據不準確
mPaint.setTextSize(mTextSize);
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
//總共的長度
float perW = mPaint.measureText("a");
int length = TextUtil.measureChineseMixLength(s);
float strWidth = perW * length;
//每一個字節的寬度
//計算一共要繪製多少行
int row =(int) (strWidth / (perW * 6));
if( strWidth % (perW * 6) != 0) {
row += 1;
}
//判斷row行的字體一共多高,便於讓它繪製在中間
float h = Math.abs(fontMetrics.bottom - fontMetrics.top);//每一行文字的高度
float totalH = h * row;//總共高度
if( totalH >= mHeight) {
row = 2;
totalH = h * row;
mSingle = true;
}
//計算整個文本離top的距離和left的距離
float oY = (mHeight - totalH) / 2;
float x1 = perW * 6;
float oX = (mWidth - x1) / 2;
int i = 0;
for( ; i < row - 1; i++) {
float y = mY + oY + i * h + h / 2;
canvas.drawText(s.substring(i * 3, i * 3 + 3), mX + oX, y, mPaint);
}
float y = mY + oY + i * h + h / 2;
if( mSingle ) {
canvas.drawText(s.substring(i * 3, i * 3 + 2) + "...", mX + oX, y, mPaint);
} else {
//繪製最後一行文字
canvas.drawText(s.substring(i * 3, s.length()), mX + oX, y, mPaint);
}
}
}
public void setBackColor(int backColor) {
this.mBackColor = backColor;
}
public int getmX() {
return mX;
}
public void setmX(int mX) {
this.mX = mX;
}
public int getmY() {
return mY;
}
public void setmY(int mY) {
this.mY = mY;
}
public int getmWidth() {
return mWidth;
}
public void setmWidth(int mWidth) {
this.mWidth = mWidth;
}
public int getmHeight() {
return mHeight;
}
public void setmHeight(int mHeight) {
this.mHeight = mHeight;
}
public CourseItem getmCourse() {
return mCourse;
}
public void setmCourse(CourseItem mCourse) {
this.mCourse = mCourse;
}
public int getmBackColor() {
return mBackColor;
}
public void setmBackColor(int mBackColor) {
this.mBackColor = mBackColor;
}
}
在這之前先看一下一個model,它保存了這節課的詳細信息:
package cn.karent.nanhang.model;
/**
* Created by wan on 2016/12/29.
* 每一節課的信息
*/
public class CourseItem {
/**
* 名稱
*/
private String name;
/**
* 教室
*/
private String classroom;
/**
* 教師
*/
private String teacher;
/**
* 時間
*/
private String time;
/**
* 週數
*/
private String week;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getClassroom() {
return classroom;
}
public void setClassroom(String classroom) {
this.classroom = classroom;
}
public String getTeacher() {
return teacher;
}
public void setTeacher(String teacher) {
this.teacher = teacher;
}
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
public String getWeek() {
return week;
}
public void setWeek(String week) {
this.week = week;
}
}
這個也是一樣,它定義一個draw方法有課程表View調用,在這個draw裏面繪製圓角矩形(也就是背景),然後繪製文字,其中文字的計算比較難算,一開始我沒有先設置畫筆的大小,測出來的數據總是跟繪製出來看到的數據不一樣,這麼一個小問題的糾結了我半天,這裏還有一個比較坑的地方,那就是中文獲取問題,比如:
String s = "我是中文abc";
int length = s.length();
中文是佔有兩個字節的,你獲取它的長度的術後就會獲取到7,按理說應該是11的,畢竟要在那麼小的地方繪製那麼多文字,肯定是需要計算每一行繪製多少個字,但是又不能全部按照一樣的來計算,因爲”我”和’a’明顯佔的地方不一樣大吧,我這裏的做法還是按照中文=英文字母 * 2來計算,所以我需要知道有多少箇中文,我使用了正則表達式來判斷:
package cn.karent.nanhang.util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created by wan on 2016/12/29.
* 判斷時候是中文
*/
public class TextUtil {
private static Pattern mPattern = Pattern.compile("[\u4e00-\u9fa5]");
/**
* 判斷是否爲中文
* @param s
* @return
*/
public static boolean isChinese(String s) {
Matcher m = mPattern.matcher(s);
if( m.find() ) {
return true;
}
return false;
}
/**
* 測量中英文混合的字符串
* @param str
* @return
*/
public static int measureChineseMixLength(String str) {
int length = 0;
for(int i = 0; i < str.length(); i++) {
String s = str.substring(i, i + 1);
if( isChinese(s) )
length += 2;
else
length += 1;
}
return length;
}
}
還有我這裏的按照二維數組來存有哪些課,比如 數組[第幾節課][星期幾],然後創建一個Activity吧,畢竟View再強大也是需要Activity的啊:
package cn.karent.nanhang.activity;
import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.PopupWindow;
import android.widget.TextView;
import cn.karent.nanhang.R;
import cn.karent.nanhang.UI.CourseUI;
import cn.karent.nanhang.adapter.WeekAdapter;
import cn.karent.nanhang.model.CourseItem;
import cn.karent.nanhang.util.PopupWindowUtil;
import cn.karent.nanhang.util.ScreenUtil;
/**
* Created by wan on 2016/12/28.
* 課表查詢的Activity
*/
public class CourseActivity extends Activity implements View.OnClickListener{
/**
* 顯示週數的Adapter
*/
private WeekAdapter mWeekAdapter;
/**
* 顯示當前第幾周
*/
private TextView mWeekText;
/**
* 彈出框
*/
private PopupWindow mPopupWindow;
/**
* PopupWindow的寬度
*/
private int mWidth;
/**
* 取消PopupWindow
*/
private View mCancelPopupWindow;
/**
* 界面右上角的設置按鈕
*/
private TextView mSetting;
/**
* 課程信息
*/
private CourseItem[][] mCourseDetail = new CourseItem[12][7];
private CourseUI mCourseUI;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.course_layout);
mWeekText = (TextView)findViewById(R.id.course_week);
mWidth = ScreenUtil.dp2px(200);
//初始化週數
initAdapter();
mWeekText.setOnClickListener(this);
//取消popWindow的窗口
mCancelPopupWindow = findViewById(R.id.course_cancelPopup);
mCancelPopupWindow.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if( mPopupWindow != null) {
mPopupWindow.dismiss();
mPopupWindow = null;
}
return false;
}
});
//將設置按鈕顯示
mSetting = (TextView) findViewById(R.id.back_setting);
mSetting.setVisibility(View.VISIBLE);
initCourseDetail();
mCourseUI = (CourseUI) findViewById(R.id.course_detail);
mCourseUI.setChildren(mCourseDetail, 12, 7);
}
/**
* 初始化週數的Adapter
*/
private void initAdapter() {
if( mWeekAdapter != null)
return;
mWeekAdapter = new WeekAdapter(this, R.layout.check_week_layout);
mWeekAdapter.setWeekTextView(mWeekText);
for(int i = 1; i <= 20; i++) {
mWeekAdapter.add("第" + i + "周");
}
}
@Override
public void onClick(View v) {
if( mPopupWindow == null) {
mPopupWindow = PopupWindowUtil.createPopupWindow(this, mWidth, mWeekAdapter, new PopupWindowUtil.ItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
mWeekText.setText(((TextView)view).getText());
//讓popupWindow消失
mPopupWindow.dismiss();
mPopupWindow = null;
}
});
}
//計算要偏移的距離
int offsetX = ScreenUtil.dp2px(-70);
int offsetY = ScreenUtil.dp2px(9);
mPopupWindow.showAsDropDown(mWeekText, offsetX, offsetY);
}
/**
* 初始化課程信息,測試數據
*/
private void initCourseDetail() {
for(int i = 0; i < 12; i++) {
for(int j = 0; j < 7; j++) {
mCourseDetail[i][j] = null;
}
}
//週一
mCourseDetail[0][0] = new CourseItem();
mCourseDetail[0][0].setName("現代測試技術B211");
mCourseDetail[1][0] = new CourseItem();
mCourseDetail[1][0].setName("現代測試技術B211");
mCourseDetail[2][0] = new CourseItem();
mCourseDetail[2][0].setName("微機原理及應用AE203");
mCourseDetail[3][0] = new CourseItem();
mCourseDetail[3][0].setName("微機原理及應用AE203");
mCourseDetail[4][0] = new CourseItem();
mCourseDetail[4][0].setName("電磁場理論A210");
mCourseDetail[5][0] = new CourseItem();
mCourseDetail[5][0].setName("電磁場理論A210");
mCourseDetail[6][0] = new CourseItem();
mCourseDetail[6][0].setName("傳感器與電子測量A312");
mCourseDetail[7][0] = new CourseItem();
mCourseDetail[7][0].setName("傳感器與電子測量A312");
mCourseDetail[8][0] = new CourseItem();
mCourseDetail[8][0].setName("傳感器與電子測量A綜合樓南513");
mCourseDetail[9][0] = new CourseItem();
mCourseDetail[9][0].setName("傳感器與電子測量A綜合樓南513");
mCourseDetail[10][0] = new CourseItem();
mCourseDetail[10][0].setName("傳感器與電子測量A綜合樓南513");
mCourseDetail[11][0] = new CourseItem();
mCourseDetail[11][0].setName("傳感器與電子測量A綜合樓南513");
//週二
mCourseDetail[0][1] = new CourseItem();
mCourseDetail[0][1].setName("數據結構與算法B211");
mCourseDetail[1][1] = new CourseItem();
mCourseDetail[1][1].setName("數據結構與算法B211");
mCourseDetail[4][1] = new CourseItem();
mCourseDetail[4][1].setName("面向對象程序設計A307");
mCourseDetail[5][1] = new CourseItem();
mCourseDetail[5][1].setName("面向對象程序設計A307");
mCourseDetail[6][1] = new CourseItem();
mCourseDetail[6][1].setName("面向對象程序設計綜合樓南307");
mCourseDetail[7][1] = new CourseItem();
mCourseDetail[7][1].setName("面向對象程序設計綜合樓南307");
//週三
mCourseDetail[2][2] = new CourseItem();
mCourseDetail[2][2].setName("現代測試技術B211");
mCourseDetail[3][2] = new CourseItem();
mCourseDetail[3][2].setName("現代測試技術B211");
mCourseDetail[4][2] = new CourseItem();
mCourseDetail[4][2].setName("現代測試技術B211");
mCourseDetail[5][2] = new CourseItem();
mCourseDetail[5][2].setName("現代測試技術B211");
//週四
mCourseDetail[0][3] = new CourseItem();
mCourseDetail[0][3].setName("面向對象程序設計A309");
mCourseDetail[1][3] = new CourseItem();
mCourseDetail[1][3].setName("面向對象程序設計A309");
mCourseDetail[2][3] = new CourseItem();
mCourseDetail[2][3].setName("傳感器與電子測量B309");
mCourseDetail[3][3] = new CourseItem();
mCourseDetail[3][3].setName("傳感器與電子測量B309");
//週五
mCourseDetail[0][4] = new CourseItem();
mCourseDetail[0][4].setName("數據結構與算法B207");
mCourseDetail[1][4] = new CourseItem();
mCourseDetail[1][4].setName("數據結構與算法B207");
mCourseDetail[2][4] = new CourseItem();
mCourseDetail[2][4].setName("微機原理及應用AE203");
mCourseDetail[3][4] = new CourseItem();
mCourseDetail[3][4].setName("微機原理及應用AE203");
mCourseDetail[8][4] = new CourseItem();
mCourseDetail[8][4].setName("形式與政策2E301");
mCourseDetail[9][4] = new CourseItem();
mCourseDetail[9][4].setName("形式與政策2E301");
mCourseDetail[10][4] = new CourseItem();
mCourseDetail[10][4].setName("形式與政策2E301");
//週六
mCourseDetail[0][5] = new CourseItem();
mCourseDetail[0][5].setName("數據結構與算法綜合樓南303");
mCourseDetail[1][5] = new CourseItem();
mCourseDetail[1][5].setName("數據結構與算法綜合樓南303");
mCourseDetail[4][5] = new CourseItem();
mCourseDetail[4][5].setName("數據結構與算法綜合樓南303");
mCourseDetail[5][5] = new CourseItem();
mCourseDetail[5][5].setName("數據結構與算法綜合樓南303");
}
}
在androidManifest裏面註冊一下:
<!--顯示課表的Activity-->
<activity android:name=".activity.CourseActivity">
</activity>
最後附上所有源碼的鏈接:源碼