[仿南航app开发日记7]自绘漂亮的课程表界面

前言

前面的博客里面分别讲到了自定义ViewGroup(侧滑菜单)、自定义Dialog(登陆对话框以及背景透明进度条)、使用PopupWindow(下拉方式选择年份),那么今天要讲的是自绘课程表界面,属于android绘图知识,在这之前我也写过课程表界面,不过当时使用的GridView+Adapter来制作课程表,不过效果不是很好,链接如下:

http://blog.csdn.net/supervictim/article/details/52809516

早就准备重写了,今天就直接重写了整个布局,这次是采用绘图的方式绘制出来的,先看下效果:
这里写图片描述

代码修改

在上一篇使用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>

最后附上所有源码的链接:源码

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章