目標
簡單的課程表APP,顯示課程,學期以及第幾周,效果圖如下:
問題與解決
- 動態添加控件時,對於可以指定
style
的構造函數如View(Context, AttributeSet, defStyleAttr: Int, defStyleRes: Int)
,需要使用new View(new ContextThemeWrapper(this, styleRes))
方式 - 上述方式創建的控件,只有style中的少數值如
padding, textColor
等有效果,並不是所有值都有效 - GridLayout中指定weight時,空白單元格也需要添加控件,否則會出現空白行列消失的問題
- GridLayout動態添加控件時需要設置LayoutParameters.rowSpec與columnSpec,具體方法爲
// ROW_WEIGHT 與 COLUMN_WEIGHT 爲float型,如1.0f ridLayout.Spec rowSpec = GridLayout.spec(ROW_INDEX, ROW_SPAN, ROW_WEIGHT); GridLayout.Spec columnSpec = GridLayout.spec(COLUMN_INDEX, COLUMN_SPAN, COLUMN_WIEGHT); GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(rowSpec, columnSpec);
android:layout_gravity="fill"
可以使單元格元素鋪滿寬高- 指定
layout_weight
時最好一併指定layout_width=0
設計與實現
界面設計
界面構建: 使用GridLayout作爲每一節課的佈局,使用AppBarLayout實現向上滾動隱藏最上方周列表的效果。因爲要實現添加課程的功能,因此採用動態添加每一節課的子控件方式,需要使用LayoutParameters設置控件的屬性。
佈局文件: 頂部使用AppBar顯示周信息,下面使用GridLayout顯示課表,每一節課使用CardView+TextView
顯示,空的小節需要使用一個空View設置,通過控制columnWeight
使控件均分剩餘空間。
- CoordinatorLayout - 配合
NestedScrollView
與AppBarLayout
實現上滑隱藏頂部效果- FloatingActionBuuton - 左下角的設置按鈕
- NestedScrollView - 設置
app:layout_behavior="@string/appbar_scrolling_view_behavior">
- GridLayout - 課表,每一小節爲一行,使用
rowSpan
設置多個小節的課
- GridLayout - 課表,每一小節爲一行,使用
- AppBarLayout
- LinearLayout - 頂部第幾周,設置
app:layout_scrollFlags="scroll|enterAlways"
- LinearLayout - 頂部第幾周,設置
實現與代碼
類ClassInfo爲數據對象,其它的學期、第一週開始日期爲靜態變量。啓動時獲取數據,然後遍歷所有課程,創建CardView或空View添加到GridLayout中。
佈局文件代碼
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/main_float_btn_setting"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
app:backgroundTint="@color/colorAccent"
android:clickable="true"
android:focusable="true"
android:src="@drawable/sqsetting"/>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<GridLayout
android:id="@+id/main_grid_layout_all_class"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:columnCount="6"
android:orientation="vertical"
android:useDefaultMargins="true"
android:background="@color/whiteSmoke"
android:rowCount="13">
<TextView
style="@style/TextViewTimeTable"
android:text="@string/timeClass1" />
<TextView
style="@style/TextViewTimeTable"
android:text="@string/timeClass2" />
<TextView
style="@style/TextViewTimeTable"
android:text="@string/timeClass3" />
<TextView
style="@style/TextViewTimeTable"
android:text="@string/timeClass4" />
<TextView
style="@style/TextViewTimeTable"
android:text="@string/timeClass5" />
<TextView
style="@style/TextViewTimeTable"
android:text="@string/timeClass6" />
<TextView
style="@style/TextViewTimeTable"
android:text="@string/timeClass7" />
<TextView
style="@style/TextViewTimeTable"
android:text="@string/timeClass8" />
<TextView
style="@style/TextViewTimeTable"
android:text="@string/timeClass9" />
<TextView
style="@style/TextViewTimeTable"
android:text="@string/timeClass10" />
<TextView
style="@style/TextViewTimeTable"
android:text="@string/timeClass11" />
<TextView
style="@style/TextViewTimeTable"
android:text="@string/timeClass12" />
<TextView
style="@style/TextViewTimeTable"
android:text="@string/timeClass13" />
</GridLayout>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.appbar.AppBarLayout
app:elevation="0dp"
android:background="@color/colorAccent"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
app:layout_scrollFlags="scroll|enterAlways"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:paddingTop="3dp"
android:paddingBottom="3dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<LinearLayout
android:layout_width="45dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/main_text_view_week_index"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="第一週"
android:textStyle="bold"
android:textColor="@color/whiteSmoke"
android:textSize="14sp" />
<TextView
android:id="@+id/main_text_view_semester"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="研一上"
android:textColor="@color/whiteSmoke"
android:textSize="13sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/main_linear_weekdays"
android:orientation="horizontal"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
style="@style/TextViewWeekDay"
android:text="@string/monday" />
<TextView
style="@style/TextViewWeekDay"
android:text="@string/tuesday" />
<TextView
style="@style/TextViewWeekDay"
android:text="@string/wednesday" />
<TextView
style="@style/TextViewWeekDay"
android:text="@string/thursday" />
<TextView
style="@style/TextViewWeekDay"
android:text="@string/friday" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
MainActivity代碼
package com.example.lessonschedule;
// import .......
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private static ArrayList<ClassInfo> allClasses;
private static int semesterIndex, grade;
private static String teachWeekStartDate;
private GridLayout allClassGrid;
private TextView semesterTextView, weekIndexTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
allClassGrid = findViewById(R.id.main_grid_layout_all_class);
semesterTextView = findViewById(R.id.main_text_view_semester);
weekIndexTextView = findViewById(R.id.main_text_view_week_index);
findViewById(R.id.main_float_btn_setting).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(MainActivity.this, SettingActivity.class));
}
});
if (allClasses == null) getStoredData();
addAllClass();
setSemesterWeek();
}
private void setSemesterWeek() {
final String[] num2ChineseNum = new String[]{null, "一", "二", "三"};
if (grade > 0 && grade <= 3 && semesterIndex >= 0 && semesterIndex <= 1)
semesterTextView.setText(String.format("研%s%s", num2ChineseNum[grade], (semesterIndex == 0 ? "上" : "下")));
int dayOfWeek = GregorianCalendar.getInstance().get(Calendar.DAY_OF_WEEK);
// 週日是第一天
if (dayOfWeek >= 2 && dayOfWeek <= 6) {
LinearLayout linearLayout = findViewById(R.id.main_linear_weekdays);
linearLayout.getChildAt(dayOfWeek - 2).setBackgroundColor(getResources().getColor(R.color.colorAccentDark));
}
try {
Date now = new Date();
Date start = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA).parse(teachWeekStartDate);
if (start != null) {
Log.e(TAG, "setSemesterWeek: " + start + ", now: " + now + " dif: " + (now.getTime() - start.getTime()));
long weekIndex = (now.getTime() - start.getTime()) / (7L * 24 * 60 * 60 * 1000) + 1;
weekIndexTextView.setText(String.format(Locale.CHINA, "第%d周", weekIndex));
}
} catch (Exception e) {
Log.e(TAG, "getStoredData: incorrect date format");
}
}
private void addAllClass() {
int k = 0;
for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= 13; ) {
if (k < allClasses.size() && allClasses.get(k).getWeekdayIndex() == i && allClasses.get(k).getClassStartIndex() == j) {
ClassInfo tmpClassInfo = allClasses.get(k);
TextView textView = new TextView(new ContextThemeWrapper(this, R.style.TextViewClassInfo));
textView.setText(tmpClassInfo.toClassInfoString());
textView.setLayoutParams(new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
CardView cardView = new CardView(new ContextThemeWrapper(this, R.style.CardViewClass));
cardView.addView(textView);
cardView.setCardBackgroundColor(tmpClassInfo.getBgColor());
GridLayout.Spec rowSpec = GridLayout.spec(j - 1, tmpClassInfo.getClassPeriod());
GridLayout.Spec columnSpec = GridLayout.spec(i, 1.0f);
GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(rowSpec, columnSpec);
layoutParams.setGravity(Gravity.FILL);
layoutParams.height = 0;
layoutParams.width = 0;
allClassGrid.addView(cardView, layoutParams);
j += tmpClassInfo.getClassPeriod();
k++;
} else {
View child = new TextView(this);
GridLayout.Spec rowSpec = GridLayout.spec(j - 1, 1);
GridLayout.Spec columnSpec = GridLayout.spec(i, 1, 1.0f);
GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(rowSpec, columnSpec);
layoutParams.width = 0;
layoutParams.height = GridLayout.LayoutParams.WRAP_CONTENT;
layoutParams.setGravity(Gravity.FILL);
child.setBackgroundColor(getResources().getColor(R.color.lightGray, null));
allClassGrid.addView(child, layoutParams);
j += 1;
}
}
}
}
private void getStoredData() {
allClasses = new ArrayList<>();
allClasses.add(new ClassInfo("計算數論", "3A112", 1, 15, 1, 6, 2));
allClasses.add(new ClassInfo("信號與信息處理", "3C101", 2, 16, 1, 11, 3));
allClasses.add(new ClassInfo("高級計算機網絡", "3B202", 1, 16, 2, 3, 3));
allClasses.add(new ClassInfo("高級計算機網絡", "3C101", 8, 12, 4, 1, 2));
allClasses.add(new ClassInfo("計算數論", "3A112", 1, 14, 4, 6, 2));
semesterIndex = 1;
grade = 1;
teachWeekStartDate = "2020-02-17";
}
}
ClassInfo類代碼
package com.example.lessonschedule;
// import ...
public class ClassInfo {
static enum ColorSupported {
Red, Pink, Purple, Deep_Purple, Indigo, Blue, Light_Blue, Cyan, Teal, Green, Light_Green, Lime, Amber, Orange
}
static final Map<ColorSupported, Integer> colorEnum2Color = new HashMap<ColorSupported, Integer>() {{
put(ColorSupported.Red, Color.rgb(244, 67, 54));
put(ColorSupported.Pink, Color.rgb(233, 30, 99));
put(ColorSupported.Purple, Color.rgb(156, 39, 176));
put(ColorSupported.Deep_Purple, Color.rgb(103, 58, 183));
put(ColorSupported.Indigo, Color.rgb(63, 81, 181));
put(ColorSupported.Blue, Color.rgb(33, 150, 243));
put(ColorSupported.Light_Blue, Color.rgb(3, 169, 244));
put(ColorSupported.Cyan, Color.rgb(0, 188, 212));
put(ColorSupported.Teal, Color.rgb(0, 150, 136));
put(ColorSupported.Green, Color.rgb(76, 175, 80));
put(ColorSupported.Light_Green, Color.rgb(139, 195, 74));
put(ColorSupported.Lime, Color.rgb(205, 220, 57));
put(ColorSupported.Amber, Color.rgb(255, 193, 7));
put(ColorSupported.Orange, Color.rgb(255, 152, 0));
}};
private String name;
private String location;
private int weekdayIndex;
private int startWeek, endWeek;
private int classStartIndex, classPeriod;
private Integer bgColor;
ClassInfo(String name, String location, int startWeek, int endWeek, int weekdayIndex, int classStartIndex, int classPeriod) {
this.name = name;
this.location = location;
this.startWeek = startWeek;
this.endWeek = endWeek;
this.classStartIndex = classStartIndex;
this.classPeriod = classPeriod;
this.weekdayIndex = weekdayIndex;
this.bgColor = colorEnum2Color.get(ColorSupported.values()[(int) (Math.random() * ColorSupported.values().length)]);
}
ClassInfo(String name, String location, int startWeek, int endWeek, int weekdayIndex, int classStartIndex, int classPeriod, ColorSupported bgColor) {
this(name, location, startWeek, endWeek, weekdayIndex, classStartIndex, classPeriod);
this.bgColor = colorEnum2Color.get(bgColor);
}
String toClassInfoString() {
return name + "\n" + location + "\n" + startWeek + "~" + endWeek + "周";
}
// getter and setter
}