Создание нестандартного компонента на основе ListView

от автора

Для приложения под Android мне понадобился элемент интерфейса, отдаленно напоминающий DatePicker. Он должен уметь:

  • прокручивать список от начала и до конца (но не по кругу), так чтобы выделять центральный элемент.
  • по мере удаления элемента от центра компонента изменять шрифт и прозрачность цифр
  • “доводить“ список до нужного элемента
  • отображать заданное количество элементов на экране
  • определять направление скроллинга (вверх или вниз)
  • рисовать тень для содержимого текстовых окон

Должен получиться компонент подобного вида:

Унаследуем наш компонент RollView от LinearLayout с дочерним элементом ListView. Внутри компонента реализуем интерфейс OnScrollListener для определения поведения ListView при скроллинге.

public class RollView extends LinearLayout implements OnScrollListener{ private final ListView innerListView; } 

В конструкторе инициализируем ListView через xml файл и присваиваем слушателя.
Для представления данных создадим внутренний адаптер с переопределенным методом getView():

private class RollAdapter extends ArrayAdapter<String> {  private final LayoutInflater mInflater; @Override public View getView(int position, View convertView, ViewGroup parent) {	 	       if (convertView == null){ 			convertView = mInflater.inflate(R.layout.roll_view_adapter, null); 			convertView.setLayoutParams(mParams); 		} 		TextView tv = (TextView) convertView.findViewById(R.id.text); 		tv.setTag(position); // записываем позицию элемента 		tv.setText(getItem(position)); 		convertView.setTag(tv); //записываем ссылку на TextView в тег 		if (!listViews.contains(convertView)) 			listViews.add(convertView); // в список для последующего обновления размера текста 		return convertView;       } } 

Все View из метода getView будем записывать в ArrayList, чтобы изменять их параметры. Метод refreshLayoutParams() задает размеры для элементов списка в зависимости от количества видимых элементов. Больше в классе адаптера ничего делать не будем.

Для того, чтобы можно было сдвинуть первый элемент списка в середину добавим в начало и конец массива пустые строки.
Теперь нужно обработать скроллинг в методах onScroll и onScrollStateChanged:

private int lastFirstVisibleElement; // индекс предыдущего "первого видимого элемента" для определения направления скроллинга private int centralIndex; //индекс элемента находящегося в центре @Override 	public void onScroll(AbsListView view, int firstVisibleItem, 			int visibleItemCount, int totalItemCount) { 		refreshTextViews(); //обновление размера текста и прозрачности 		//Для определения направления скроллинга 		if (lastFirstVisibleElement > firstVisibleItem){ 			Log.i("RollView", "Scroll up"); 		} 		else if (lastFirstVisibleElement < firstVisibleItem){ 			Log.i("RollView", "Scroll down"); 		} 		lastFirstVisibleElement = firstVisibleItem; 	}  	@Override 	public void onScrollStateChanged(AbsListView view, int scrollState) { 		//После отпускания пальца 		if (scrollState == SCROLL_STATE_IDLE){ 			//Плавная доводка 			smoothScrollToPositionFromTop(centralIndex - totalElementVisible / 2 , 0, 1); 	} 

Метод refreshTextViews() отвечает за изменение размера текста и прозрачности в зависимости от положения элемента:

public void refreshTextViews(){ 		float maxTextSize = 0; 				 		for (View v : listViews){ 		int centerOfViewY = v.getBottom() - (mAdapter.mParams.height / 2); 		ShadowTextView tv = (ShadowTextView) v.getTag(); 		float coefficient = (Math.abs(centerOfViewY - mAdapter.centerLineY)) / (float)mAdapter.centerLineY; 		float scale = 0; //Если коэффициент больше 1 - значит элемент за пределами видимости 		if (coefficient < 1) 			scale = Math.abs(coefficient - 1); 		tv.setAlpha(scale); //Определяем элемент с наибольшим размером текста для доводки к нему float textSize = CENTRAL_TEXT_SIZE * scale; 	if (textSize > maxTextSize){ 		maxTextSize = textSize; 		centralIndex = (Integer) tv.getTag(); 		} 		tv.setTextSize(textSize); 		} 	}  

Осталось добавить тени для текста. Для этого создадим унаследованный от TextView компонент ShadowTextView. Для рисования текста с тенями нужно создать кисть(Paint) и задать ей параметры:

Параметры кисти

private final Paint mPaint = new Paint();

// Параметры кисти для рисования теней 	private void initPaint(){ 		mPaint.setAntiAlias(true); 		mPaint.setTextSize(getTextSize()); 		mPaint.setColor(Color.WHITE); 		mPaint.setStrokeWidth(2.0f); 		mPaint.setStyle(Paint.Style.FILL); 		mPaint.setTextAlign(Paint.Align.CENTER); 		mPaint.setShadowLayer(10.0f, 0.0f, 0.0f, Color.BLACK); 	} 

и в методе onDraw() перерисовать компонент:

private final Rect mBounds = new Rect(); // границы текста @Override 	protected void onDraw(Canvas canvas){ 		canvas.drawColor(Color.TRANSPARENT); 		int x = getWidth() / 2; 		int y = (getHeight() + mBounds.height()) / 2; 		canvas.drawText(getText().toString(),  x, y, mPaint); 		} 	} 

Для перерисовки теней из RollView добавим метод redraw():

public void redraw(){ 		text = getText().toString(); 		mPaint.setTextSize(getTextSize()); 		mPaint.getTextBounds(text, 0, getText().toString().length()	, mBounds); 		invalidate(); 	} 

Осталось только заменить TextView в на ShadowTextView и вызвать в методе refreshTextViews метод tv.redraw();
Теперь для получения выбранного пользователем значения осталось только добавить методы getCurrentItemValue() и getCurrentItemIndex().
Наглядная демонстрация работы:

Ссылка на полный проект:
https://bitbucket.org/msinchevskaya/rollview

ссылка на оригинал статьи http://habrahabr.ru/post/225649/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *