[仿南航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>

最後附上所有源碼的鏈接:源碼

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