Android 自定義價格日曆控制元件
介紹
上個星期專案有一個日曆價格的需求,類似一個商品在不同的日期價格可能會不同,由於時間給得特別緊所以打算找個合適的開源專案進行修改。參考了網上大多數是通過繼承view直接draw一個monthView,然後通過listview來實現monthView的複用。但是繼承view通過draw來實現月份日曆比較麻煩,如果需要修改樣式或者新增額外的資訊會比較麻煩,所以為什麼不用gridview來實現月份的顯示呢?這樣monthview的每個佈局都是寫在xml裡的,別人參考你的改起來也方便,並且大多於自己的需求不符合,所以自己實現了一個價格日曆,這裡分享出來給大家參考。先貼一下效果圖
實現思路
前面提到了用gridView來顯示月份,要實現日曆肯定有很多月份,所以我們用viewPager+gridView來實現,這裡我們不僅要實現效果 還要以後遇到類似的功能只需要簡單使用而不是重寫,所以考慮自定義view中的組合型別。關於自定義view我之前有多篇文章講到,概念性的東西就不提了,直接開始。
具體實現
組合控制元件其實就是將多個控制元件組合在一個控制元件裡並且在該控制元件中實現一些互動和處理,是為了封裝一些內部特性,方便直接使用。
步驟1 獲取內部控制元件
。上面我們提到用viewPager+gridView來實現,當然還有一個顯示月份的textview和兩個button。那麼第一步就是獲取到這些控制元件。宣告變數並在onFinishInflate() 方法中獲取。
程式碼如下:
public class CommonCalendarView extends FrameLayout implements View.OnClickListener {
private ViewPager mViewPager;
private TextView mMonthTv;
private Context mContext;
private android.widget.ImageButton mLeftMonthBtn;
private android.widget.ImageButton mRightMonthBtn;
public CommonCalendarView(Context context) {
this(context,null);
}
public CommonCalendarView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public CommonCalendarView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
View view = LayoutInflater.from(mContext).inflate(R.layout.activity_page_calendar_price,this,true);
this.mViewPager = (ViewPager) view.findViewById(R.id.viewPager);
this.mRightMonthBtn = (ImageButton) view.findViewById(R.id.right_month_btn);
this.mMonthTv = (TextView) view.findViewById(R.id.month_tv);
this.mLeftMonthBtn = (ImageButton) view.findViewById(R.id.left_month_btn);
this.mLeftMonthBtn.setOnClickListener(this);
this.mRightMonthBtn.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.left_month_btn:
mViewPager.setCurrentItem(mViewPager.getCurrentItem()-1,true);
break;
case R.id.right_month_btn:
mViewPager.setCurrentItem(mViewPager.getCurrentItem()+1,true);
break;
}
}
}
程式碼大家肯定一看就知道,獲取控制元件,為左右兩個按鈕設定點選實現。直接呼叫viewPager的setCurrentItem();
步驟2 為ViewPager設定介面卡顯示年月
有了基本的控制元件以後那麼我們還需要用來顯示資料,從使用者的角度來說我可能只想告訴你最大最小日期,或者最大年份就可以了。這裡提供一個介面獲取最大年份並且宣告最大最小日期變數,程式碼如下:
...
private DatePickerController mController;
private CalendarAdapter adapter;
private Date maxDate;
private Date minDate;
public void setMaxDate(Date maxDate) {
this.maxDate = maxDate;
}
public void setMinDate(Date minDate) {
this.minDate = minDate;
}
public interface DatePickerController {
int getMaxYear();
void onDayOfMonthSelected(int year, int month, int day);//日期選擇
void onDayOfMonthAndDataSelected(int year,int month,int day,List obj);//日期附加資訊選擇
//展示其它屬性(用於擴充套件資料日期相等時設定顯示效果)
void showOtherFields(Object obj, View view, int gridItemYear, int gridItemMonth, int gridItemDay);
//獲取附加資訊
Map<String,List> getDataSource();
}
...
為外部提供控制元件初始化方法用來獲取資料
public void init(DatePickerController controller){
if (controller==null){
mController = new DatePickerController() {
@Override
public int getMaxYear() {
return DateUtils.getToYear()+1;
}
@Override
public void onDayOfMonthSelected(int year, int month, int day) {
Toast.makeText(mContext, String.format("%s-%s-%s", year,StringUtils.leftPad(String.valueOf(month),2,"0"),
StringUtils.leftPad(String.valueOf(day),2,"0")), Toast.LENGTH_SHORT).show();
}
@Override
public void onDayOfMonthAndDataSelected(int year, int month, int day, List obj) {
}
@Override
public void showOtherFields(Object obj, View view, int gridItemYear, int gridItemMonth, int gridItemDay) {
}
@Override
public Map<String, List> getDataSource() {
return null;
}
};
}else{
mController = controller;
}
this.mYearMonthMap = mController.getDataSource();
adapter = new CalendarAdapter(mContext);
mViewPager.setPageTransformer(true,new DepthPageTransformer());
mViewPager.setAdapter(adapter);
if (minDate!=null){
mMonthTv.setText(String.format("%s年%s月",DateUtils.getYear(minDate), StringUtils.leftPad(String.valueOf(DateUtils.getMonth(minDate)),2,"0")));
}else{
mMonthTv.setText(String.format("%s年%s月",DateUtils.getToYear(), StringUtils.leftPad(String.valueOf(DateUtils.getToMonth()),2,"0")));
}
mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
mMonthTv.setText(adapter.getPageTitle(position));
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
}
上面的程式碼如果使用者沒有提供給我們,我們預設資料來源提供maxYear 為今年+1,預設點選事件實現Toast提示。
否則的話直接為controller賦值。
同時設定viewPager的adapter。
來看adapter的程式碼
class CalendarAdapter extends PagerAdapter implements AdapterView.OnItemClickListener {
protected static final int MONTHS_IN_YEAR = 12;
private final Calendar calendar = Calendar.getInstance();
private Integer firstMonth = calendar.get(Calendar.MONTH);
private LayoutInflater inflater;
private Integer lastMonth = (calendar.get(Calendar.MONTH) - 1) % MONTHS_IN_YEAR;
private Integer startYear = calendar.get(Calendar.YEAR);
public CalendarAdapter(Context context) {
inflater = LayoutInflater.from(context);
mContext = context;
if (maxDate!=null){
lastMonth = DateUtils.getMonth(maxDate)-1;
}
if (minDate!=null){
startYear = DateUtils.getYear(minDate);
firstMonth = DateUtils.getMonth(minDate)-1;
}
}
@Override
public CharSequence getPageTitle(int position) {
int year = position / MONTHS_IN_YEAR + startYear + ((firstMonth + (position % MONTHS_IN_YEAR)) / MONTHS_IN_YEAR);
int month = (firstMonth + (position % MONTHS_IN_YEAR)) % MONTHS_IN_YEAR;
return String.format("%s年%s月",year, StringUtils.leftPad(String.valueOf(month+1),2,"0"));
}
@Override
public int getCount() {
int maxYear = mController.getMaxYear();
int minYear = calendar.get(Calendar.YEAR) ;
if (maxDate!=null){
maxYear = DateUtils.getYear(maxDate);
}
if (minDate!=null){
minYear = DateUtils.getYear(minDate);
}
int itemCount = (maxYear-minYear+1) * MONTHS_IN_YEAR;
if (firstMonth != -1)
itemCount -= firstMonth;
if (lastMonth != -1)
itemCount -= (MONTHS_IN_YEAR - lastMonth) - 1;
return itemCount;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
GridView mGridView = mViewMap.get(position);
if (mGridView ==null){
mGridView = (GridView) inflater.inflate(R.layout.item_page_month_day, container, false);
mViewMap.put(position,mGridView);
}
int year = position / MONTHS_IN_YEAR + startYear + ((firstMonth + (position % MONTHS_IN_YEAR)) / MONTHS_IN_YEAR);
int month = (firstMonth + (position % MONTHS_IN_YEAR)) % MONTHS_IN_YEAR;
DateBean dateBean = new DateBean(year, month + 1);
mGridView.setOnItemClickListener(this);
mGridView.setAdapter(new MyGridAdapter(dateBean));
container.addView(mGridView);
return mGridView;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
MyGridAdapter gridAdapter = (MyGridAdapter) parent.getAdapter();
int day = (int) gridAdapter.getItem(position);
if (day == -1) {
return;
}
DateBean bean = gridAdapter.getDateBean();
List<ProductDatePrice> list = gridAdapter.getProductDatePriceList();
if (mController!=null){
if (list!=null&&!list.isEmpty()){
mController.onDayOfMonthAndDataSelected(bean.currentYear,bean.currentMonth,day+1,list);
}else{
mController.onDayOfMonthSelected(bean.currentYear,bean.currentMonth,day+1);
}
}
}
}
通過getCount方法獲取總count,確定要顯示的總數。
在instantiateItem方法中根據position獲取當前年月,然後為gridView設定adapter。
gridView程式碼如下:
class MyGridAdapter extends BaseAdapter {
private DateBean mDateBean;
private int days;
private int dayOfWeeks;
private List mProductDatePriceList;
public DateBean getDateBean() {
return mDateBean;
}
public MyGridAdapter(DateBean dateBean) {
this.mDateBean = dateBean;
if (mYearMonthMap!=null){
this.mProductDatePriceList = mYearMonthMap.get(String.format("%s-%s", dateBean.currentYear, StringUtils.leftPad(dateBean.currentMonth + "", 2, "0")));
}
GregorianCalendar c = new GregorianCalendar(dateBean.currentYear, dateBean.currentMonth - 1, 0);
days = DateUtils.getDaysOfMonth(dateBean.currentYear, dateBean.currentMonth); //返回當前月的總天數。
dayOfWeeks = c.get(Calendar.DAY_OF_WEEK);
if (dayOfWeeks == 7) {
dayOfWeeks = 0;
}
}
public List getProductDatePriceList() {
return mProductDatePriceList;
}
@Override
public int getCount() {
return days + dayOfWeeks;
}
@Override
public Object getItem(int i) {
if (i < dayOfWeeks) {
return -1;
} else {
return i - dayOfWeeks;
}
}
@Override
public long getItemId(int i) {
return 0;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
GridViewHolder viewHolder ;
if (view == null) {
view = LayoutInflater.from(mContext).inflate(R.layout.item_day, viewGroup, false);
viewHolder = new GridViewHolder();
viewHolder.mTextView = (TextView) view.findViewById(R.id.day_tv);
viewHolder.mPriceTv = (TextView) view.findViewById(R.id.price_tv);
viewHolder.mLineView = view.findViewById(R.id.line_view);
view.setTag(viewHolder);
} else {
viewHolder = (GridViewHolder) view.getTag();
}
int item = (int) getItem(i);
if (item == -1) {
viewHolder.mTextView.setText("");
viewHolder.mPriceTv.setText("");
} else {
viewHolder.mTextView.setText(String.valueOf(item + 1));
viewHolder.mPriceTv.setText("");
if (i%7==0||i%7==6){
viewHolder.mTextView.setActivated(true);
}else{
viewHolder.mTextView.setActivated(false);
}
if (mProductDatePriceList != null) {
viewHolder.mTextView.setEnabled(false);
view.setEnabled(false);
for (Object obj : mProductDatePriceList) {//用於展示價格等額外的屬性
if (mController!=null){
mController.showOtherFields(obj,view,mDateBean.currentYear,mDateBean.currentMonth,item+1);
}
}
}
}
return view;
}
}
主要是根據當前年月 獲取dayOfWeeks,本月第一天為星期幾,然後判斷需要空出幾格。
使用
簡單使用
1、xml中宣告view
<com.qiangyu.test.commoncalendarview.view.CommonCalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</com.qiangyu.test.commoncalendarview.view.CommonCalendarView>
2、獲取view並設定資料來源
public class SimpleCalendarActivity extends AppCompatActivity {
private CommonCalendarView calendarView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_simple_calendar);
this.calendarView = (CommonCalendarView) findViewById(R.id.calendarView);
this.calendarView.setMinDate(DateUtils.stringtoDate("1937-01-01","yyyy-MM-dd"));
this.calendarView.setMaxDate(DateUtils.stringtoDate("2100-01-22","yyyy-MM-dd"));
this.calendarView.init(null);
}
}
日曆新增額外資訊
1、xml中宣告view
<com.qiangyu.test.commoncalendarview.view.CommonCalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</com.qiangyu.test.commoncalendarview.view.CommonCalendarView>
2、獲取view並且設定資料來源,
public class MoreInfoCalendarActivity extends AppCompatActivity {
private CommonCalendarView calendarView;
private Map<String,List> mYearMonthMap = new HashMap<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_more_info_calendar);
List<ProductDatePrice> mDatePriceList = new ArrayList<>();
for (int i = 1; i <= 12; i++) {//構造12個月每天的價格資料
for (int j = 1; j <= 28; j++) {
ProductDatePrice price = new ProductDatePrice();
price.setPriceDate(String.format("2017-%s-%s", StringUtils.leftPad(String.valueOf(i), 2, "0"), StringUtils.leftPad(String.valueOf(j), 2, "0")));
price.setPrice(RandomUtils.nextInt(1000));
mDatePriceList.add(price);
}
}
for (ProductDatePrice productDatePrice : mDatePriceList) {//把價格資料改為同一個月的list 在一個key value裡,減少渲染介面時迴圈判斷數量
productDatePrice.getPriceDate();
String yearMonth = TextUtils.substring(productDatePrice.getPriceDate(), 0, TextUtils.lastIndexOf(productDatePrice.getPriceDate(), '-'));
List list = mYearMonthMap.get(yearMonth);
if (list == null) {
list = new ArrayList();
list.add(productDatePrice);
mYearMonthMap.put(yearMonth, list);
} else {
list.add(productDatePrice);
}
}
this.calendarView = (CommonCalendarView) findViewById(R.id.calendarView);
this.calendarView.init(new CommonCalendarView.DatePickerController() {
@Override
public int getMaxYear() {
return 2018;
}
@Override
public void onDayOfMonthSelected(int year, int month, int day) {
Toast.makeText(MoreInfoCalendarActivity.this, String.format("%s-%s-%s", year,StringUtils.leftPad(String.valueOf(month),2,"0"),
StringUtils.leftPad(String.valueOf(day),2,"0")), Toast.LENGTH_SHORT).show();
}
@Override
public void onDayOfMonthAndDataSelected(int year, int month, int day, List obj) {
if (obj==null){
return;
}
String priceDate = String.format("%s-%s-%s", year,
StringUtils.leftPad(month + "", 2, "0"), StringUtils.leftPad(String.valueOf(day), 2, "0"));
for (int i = 0; i < obj.size(); i++) {
ProductDatePrice datePrice = (ProductDatePrice) obj.get(i);
if (datePrice==null){
continue;
}
if (TextUtils.equals(datePrice.getPriceDate(),priceDate)){
Toast.makeText(MoreInfoCalendarActivity.this, datePrice.toString(), Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void showOtherFields(Object obj, View view, int gridItemYear, int gridItemMonth, int gridItemDay) {
//當你設定了資料來源之後,介面渲染會迴圈呼叫showOtherFields方法,在該方法中實現同一日期設定介面顯示效果。
ProductDatePrice productDatePrice = (ProductDatePrice) obj;
if (TextUtils.equals(productDatePrice.getPriceDate(), String.format("%s-%s-%s", gridItemYear,
StringUtils.leftPad(gridItemMonth + "", 2, "0"), StringUtils.leftPad(String.valueOf(gridItemDay), 2, "0")))) {
CommonCalendarView.GridViewHolder viewHolder = (CommonCalendarView.GridViewHolder) view.getTag();
viewHolder.mPriceTv.setText(String.format("¥ %s", productDatePrice.getPrice()));
view.setEnabled(true);
viewHolder.mTextView.setEnabled(true);
}
}
@Override
public Map<String, List> getDataSource() {
return mYearMonthMap;
}
});
}
}
結語
本偏的自定義價格日曆控制元件還有很多不完善的地方,在這裡分享出來只是拋磚引玉,希望對大家有所幫助,歡迎關注我的部落格!