前言
前面的博客里面分别讲到了自定义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>
最后附上所有源码的链接:源码