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

в 6:34, , рубрики: android, custom view, java, views, Разработка под android, метки: , , ,

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

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

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

Унаследуем наш компонент 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

Автор: m_sinch

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js