編輯:關於Android編程
在項目中有一個小功能需要實現,就是對多行文本進行排版布局,每一行的內容又分為兩部分,左邊為標題,右邊為描述,左邊內容長度不確定,右邊的內容需要對齊,如有換行也需要對齊右邊的文本。
一、效果圖
可以看到內容分成了兩部分,左邊的顏色與右邊不一致,右邊的描述文案統一對齊。
二、實現方案
以上功能,由於輸入內容輸入行數不確定,並且左邊的文案長度也不確定,因此不能直接在布局中實現,基於此這裡主要實現了以下6種方式
方案1
采用自定義控件的方式,繼承TextView,重新onDraw函數,實現如下:
/** * 計算出左邊最長的顯示字符串maxLeftWidth,之後draw每一行字符,右邊的描述從maxLeftWidth開始draw * 當一行顯示不完全時,折行並且空出maxLeftWidth的空格長度 */ public class TypographyView1 extends TextView { private Paint leftPaint = new Paint(); private Paint rightPaint = new Paint(); private int fullWidth; private float textSize; private JSONArray array; private int middlePadding = 0; float maxLeftWidth = 0; int itemSize = 0; public TypographyView1(Context context) { super(context); init(); } public TypographyView1(Context context, AttributeSet attrs) { super(context, attrs); init(); } public TypographyView1(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { textSize = getResources().getDimensionPixelSize(R.dimen.text_size_13); leftPaint.setAntiAlias(true); leftPaint.setTextSize(textSize); leftPaint.setColor(getResources().getColor(R.color.color_black_999999)); rightPaint.setAntiAlias(true); rightPaint.setTextSize(textSize); rightPaint.setColor(getResources().getColor(R.color.color_black)); middlePadding = getResources().getDimensionPixelSize(R.dimen.padding_value); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); fullWidth = getWidth();// 整個textView的寬度 } public void setText(JSONArray array) { this.array = array; if (array != null) { try { int size = itemSize = array.length(); for (int i = 0; i < size; ++i) { JSONArray o = (JSONArray) array.get(i); String key = o.getString(0); String value = o.getString(1); if (TextUtils.isEmpty(key) || TextUtils.isEmpty(value)) { itemSize--; continue; } float curWidth = leftPaint.measureText(key); if (curWidth > maxLeftWidth) { maxLeftWidth = curWidth; } } maxLeftWidth = maxLeftWidth + middlePadding; invalidate(); } catch (Exception e) { } } } boolean setHeight = false; @Override protected void onDraw(Canvas canvas) { if (array == null) { return; } int lineCount = 0; try { JSONArray item; float offsetY; for (int i = 0; i < itemSize; ++i) { item = (JSONArray) array.get(i); offsetY = (lineCount + 1) * textSize; canvas.drawText(item.getString(0), 0, offsetY, leftPaint); String value = item.getString(1); float valueWidth = rightPaint.measureText(value); if (valueWidth > fullWidth - maxLeftWidth) {// 一行顯示不完 char[] textCharArray = value.toCharArray(); float charWidth; float drawWidth = maxLeftWidth; for (int j = 0; j < textCharArray.length; j++) { charWidth = rightPaint.measureText(textCharArray, j, 1); if (fullWidth - drawWidth < charWidth) { lineCount++; drawWidth = maxLeftWidth; offsetY += textSize; } canvas.drawText(textCharArray, j, 1, drawWidth, offsetY, rightPaint); drawWidth += charWidth; } } else { canvas.drawText(value, maxLeftWidth, offsetY, rightPaint); } lineCount += 2; } if (!setHeight) { setHeight((lineCount + 1) * (int) textSize); setHeight = true; } } catch (JSONException e) { e.printStackTrace(); } } }
添加了setText(JSONArray array)作為數據輸入,並且在這裡面測量了左邊title的最大寬度,之後調用invalidate觸發重繪,在onSizeChanged獲取整個控件的寬度,重繪會調用onDraw函數,這裡不需要調用super函數,TextView的onDraw函數做了非常多的操作,解析傳入的數據,分別一行一行調用canvas來進行drawText操作,當繪制描述時,先計算寬度,如果超過剩余控件說明需要換行,最後調用setHeight設置高度,這個加一個判斷條件,因為會觸發requestLayout()進行重新布局和invalidate()進行重繪,如果不加判斷會一直重繪。
方案2
方式2與方式1差不多,不同為所有計算都在onDraw函數中:
/** * 該方式與方式1很類似,只是所有的計算都放在了onDraw方法中。 */ public class TypographyView2 extends TextView { private Paint paint1 = new Paint(); private Paint paint2 = new Paint(); private int middlePadding = 0; int width; private float textSize; private JSONArray array; public TypographyView2(Context context) { super(context); init(); } public TypographyView2(Context context, AttributeSet attrs) { super(context, attrs); init(); } public TypographyView2(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { textSize = getResources().getDimensionPixelSize(R.dimen.text_size_13); paint1.setAntiAlias(true); paint1.setTextSize(textSize); paint1.setColor(getResources().getColor(R.color.color_black_999999)); paint2.setAntiAlias(true); paint2.setTextSize(textSize); paint2.setColor(getResources().getColor(R.color.color_black)); middlePadding = getResources().getDimensionPixelSize(R.dimen.padding_value); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); width = getWidth();// 整個textView的寬度 } public void setText(JSONArray array) { this.array = array; if (array != null) { invalidate(); } } boolean setHeight = false; @Override protected void onDraw(Canvas canvas) { // super.onDraw(canvas); int lineCount = 0; int size = array.length(); float maxLeftWidth = 0; float drawWidth = 0; try { for (int i = 0; i < size; ++i) { JSONArray o = (JSONArray) array.get(i); String key = o.getString(0); float v = paint1.measureText(key); if (v > maxLeftWidth) { maxLeftWidth = v; } } maxLeftWidth = maxLeftWidth + middlePadding; for (int i = 0; i < size; ++i) { JSONArray o = (JSONArray) array.get(i); String key = o.getString(0); canvas.drawText(key, 0, (lineCount + 1) * textSize, paint1); String value = o.getString(1); char[] textCharArray = value.toCharArray(); float charWidth; drawWidth = maxLeftWidth; for (int j = 0; j < textCharArray.length; j++) { charWidth = paint1.measureText(textCharArray, j, 1); if (width - drawWidth < charWidth) { lineCount++; drawWidth = maxLeftWidth; } canvas.drawText(textCharArray, j, 1, drawWidth, (lineCount + 1) * textSize, paint2); drawWidth += charWidth; } lineCount += 2; } if (!setHeight) { setHeight((lineCount + 1) * (int) textSize + 5); setHeight = true; } } catch (JSONException e) { e.printStackTrace(); } } }
該方案的實現是不太好的,方案1也是在此基礎上進行調整的,在這裡放出來只是為了說明,所有的計算不要全部放在onDraw裡面,因為該方法可能會反復調用多次,這樣就降低了性能。
方案3
將數據源拼接成SpannableString,重寫onDraw函數,根據內容draw每一個字符:
/** * 該方法,是需要顯示的內容先拼接成SpannableString,在onDraw方法中獲取所有的char字符,一個一個比較 * 當為分號是,表示為key與value的分隔符。 */ public class TypographyView3 extends TextView { private Paint leftPaint = new Paint(); private Paint rightPaint = new Paint(); int width; private String text; private float textSize; float maxLeftWidth = 0; private int middlePadding = 0; public TypographyView3(Context context) { super(context); init(); } public TypographyView3(Context context, AttributeSet attrs) { super(context, attrs); init(); } public TypographyView3(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { textSize = getResources().getDimensionPixelSize(R.dimen.text_size_13); textSize = getResources().getDimensionPixelSize(R.dimen.text_size_13); leftPaint.setAntiAlias(true); leftPaint.setTextSize(textSize); leftPaint.setColor(getResources().getColor(R.color.color_black_999999)); rightPaint.setAntiAlias(true); rightPaint.setTextSize(textSize); rightPaint.setColor(getResources().getColor(R.color.color_black)); middlePadding = getResources().getDimensionPixelSize(R.dimen.padding_value); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); width = getWidth();// 整個textView的寬度 } public void setText(JSONArray data) { if (data == null) { return; } try { int size = data.length(); for (int i = 0; i < size; ++i) { JSONArray o = (JSONArray) data.get(i); String key = o.getString(0); float v = leftPaint.measureText(key); if (v > maxLeftWidth) { maxLeftWidth = v; } } maxLeftWidth += middlePadding; SpannableStringBuilder ssb = new SpannableStringBuilder(); for (int i = 0; i < size; ++i) { addItem((JSONArray) data.get(i), ssb, i != 0); } setText(ssb, BufferType.SPANNABLE); } catch (Exception e) { } } private void addItem(JSONArray item, SpannableStringBuilder ssb, boolean breakLine) { try { if (item == null || item.length() == 0) { return; } String key = item.getString(0); String value = (item.length() >= 2) ? item.getString(1) : ""; if (TextUtils.isEmpty(key) && TextUtils.isEmpty(value)) { return; } if (breakLine) {// 換行 ssb.append("\r\n"); ssb.append("\r\n"); } SpannableString span = new SpannableString(key); // span.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorAccent)), 0, key // .length(), // Spanned.SPAN_INCLUSIVE_EXCLUSIVE); ssb.append(span); ssb.append(value); } catch (JSONException e) { e.printStackTrace(); } } @Override protected void onDraw(Canvas canvas) { // super.onDraw(canvas); int lineCount = 0; text = this.getText().toString(); if (text == null) return; char[] textCharArray = text.toCharArray(); // 已繪的寬度 float drawWidth = 0; float charWidth; Paint paint = leftPaint; for (int i = 0; i < textCharArray.length; i++) { charWidth = leftPaint.measureText(textCharArray, i, 1); if (textCharArray[i] == '\n') { lineCount++; drawWidth = 0; paint = leftPaint; continue; } if (width - drawWidth < charWidth) { lineCount++; drawWidth = maxLeftWidth; } if (i > 1 && textCharArray[i - 1] == ':') { drawWidth = maxLeftWidth; paint = rightPaint; } canvas.drawText(textCharArray, i, 1, drawWidth, (lineCount + 1) * textSize, paint); drawWidth += charWidth; } //may be need set height //setHeight((lineCount + 1) * (int) textSize + 5); } }
這裡先計算左邊title的最大寬度,同時將所有的數據拼接成一個SpannableStringBuilder,調用setText函數會觸發重繪,在onDraw函數中進行處理,由於未重新super函數,因此SpannableString的setSpan函數失效,該方案主要根據分隔符來進行分割,因此分隔符需要唯一。
方案4
采用GridLayout方式實現,但是原始控件有展示問題,因此對此進行了修改:
public class Typography4Activity extends BaseActivity { public static void start(Context context) { Intent intent = new Intent(); intent.setClass(context, Typography4Activity.class); context.startActivity(intent); } private LinearLayout root; private Paint leftPaint = new Paint(); private float textSize; private float maxLeftWidth; private int middlePadding = 0; private float maxRightWidth; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); root = (LinearLayout) LayoutInflater.from(this).inflate(R.layout.activity_typography4, null); setContentView(root); initPaint(); findViews(); loadData(); } private void initPaint() { textSize = getResources().getDimensionPixelSize(R.dimen.text_size_13); leftPaint.setAntiAlias(true); leftPaint.setTextSize(textSize); leftPaint.setColor(getResources().getColor(R.color.color_black_999999)); middlePadding = getResources().getDimensionPixelSize(R.dimen.padding_value); } private void findViews() { } private void loadData() { addGridLayout(DataSource.getArray()); TextView view = new TextView(this); view.setText("修改後的實現"); view.setGravity(Gravity.CENTER); view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 160)); root.addView(view); addModifyGridLayout(DataSource.getArray()); } private void addGridLayout(JSONArray data) { try { GridLayout layout = createGridLayout(); int size = data.length(); for (int i = 0; i < size; ++i) { JSONArray item = (JSONArray) data.get(i); String key = item.getString(0); String value = (item.length() >= 2) ? item.getString(1) : ""; GridLayout.Spec row = GridLayout.spec(i); GridLayout.Spec col1 = GridLayout.spec(0); GridLayout.Spec col2 = GridLayout.spec(1); GridLayout.LayoutParams params = new GridLayout.LayoutParams(row, col1); TextView title = getLeftTextView(key); layout.addView(title, params); params = new GridLayout.LayoutParams(row, col2); TextView desc = getRightTextView(value); layout.addView(desc, params); } root.addView(layout); } catch (Exception e) { } } @NonNull private TextView getRightTextView(String value) { TextView desc = new TextView(this); desc.setTextSize(13); desc.setTextColor(getResources().getColor(R.color.black)); desc.setText(value); return desc; } @NonNull private TextView getLeftTextView(String key) { TextView title = new TextView(this); title.setText(key); title.setPadding(0, middlePadding, middlePadding, 0); title.setTextColor(getResources().getColor(R.color.color_black_999999)); title.setTextSize(13); return title; } private void addModifyGridLayout(JSONArray data) { try { calculateLeftMaxWidth(data); GridLayout layout = createGridLayout(); int size = data.length(); for (int i = 0; i < size; ++i) { JSONArray item = (JSONArray) data.get(i); GridLayout.Spec row = GridLayout.spec(i); String key = item.getString(0); GridLayout.Spec col1 = GridLayout.spec(0); GridLayout.LayoutParams params = new GridLayout.LayoutParams(row, col1); TextView title = getLeftTextView(key); layout.addView(title, params); String value = (item.length() >= 2) ? item.getString(1) : ""; GridLayout.Spec col2 = GridLayout.spec(1); params = new GridLayout.LayoutParams(row, col2); TextView desc = getRightTextView(value); params.width = (int) maxRightWidth; params.height = ViewGroup.LayoutParams.WRAP_CONTENT; layout.addView(desc, params); } root.addView(layout); } catch (Exception e) { } } private void calculateLeftMaxWidth(JSONArray data) { try { DisplayUtil.init(this);// 這個可以在應用程序起來的時候init int size = data.length(); for (int i = 0; i < size; ++i) { JSONArray o = (JSONArray) data.get(i); String key = o.getString(0); String value = o.getString(1); if (TextUtils.isEmpty(key) || TextUtils.isEmpty(value)) { continue; } float curWidth = leftPaint.measureText(key); if (curWidth > maxLeftWidth) { maxLeftWidth = curWidth; } } maxLeftWidth = maxLeftWidth + middlePadding; maxRightWidth = DisplayUtil.screenWidth - DisplayUtil.dp2px(this, 32 + 10) - maxLeftWidth; } catch (Exception e) { } } private GridLayout createGridLayout() { GridLayout layout = new GridLayout(this); layout.setColumnCount(2); //layout.setRowCount(5); layout.setOrientation(GridLayout.HORIZONTAL); return layout; } }
如果直接創建一個GridLayout,裡面添加每一項,如果描述過長都導致顯示不全,這個是系統的一個bug,計算的寬度有問題,因此需要對此方案進行更改。
更改方式為先計算左邊占用的最大寬度,在添加右邊的項時,設置布局參數控制最大的長度。
方案5
采用每一行一個布局,手動一行一行進行添加:
public class Typography5Activity extends BaseActivity { public static void start(Context context) { Intent intent = new Intent(); intent.setClass(context, Typography5Activity.class); context.startActivity(intent); } private LinearLayout root; private Paint leftPaint = new Paint(); private float textSize; private float maxLeftWidth; private int middlePadding = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); root = (LinearLayout) LayoutInflater.from(this).inflate(R.layout.activity_typography5, null); setContentView(root); initPaint(); loadData(); } private void initPaint() { textSize = getResources().getDimensionPixelSize(R.dimen.text_size_13); leftPaint.setAntiAlias(true); leftPaint.setTextSize(textSize); leftPaint.setColor(getResources().getColor(R.color.color_black_999999)); middlePadding = getResources().getDimensionPixelSize(R.dimen.padding_value); } private void loadData() { JSONArray array = DataSource.getArray(); calculateLeftMaxWidth(array); if (array != null) { try { int size = array.length(); for (int i = 0; i < size; ++i) { JSONArray o = (JSONArray) array.get(i); String key = o.getString(0); String value = o.getString(1); addItem(key, value); } } catch (Exception e) { } } } private void calculateLeftMaxWidth(JSONArray data) { try { int size = data.length(); for (int i = 0; i < size; ++i) { JSONArray o = (JSONArray) data.get(i); String key = o.getString(0); String value = o.getString(1); if (TextUtils.isEmpty(key) || TextUtils.isEmpty(value)) { continue; } float curWidth = leftPaint.measureText(key); if (curWidth > maxLeftWidth) { maxLeftWidth = curWidth; } } maxLeftWidth = maxLeftWidth + middlePadding; } catch (Exception e) { } } private void addItem(String key, String value) { LinearLayout layout = getItemLayout(); TextView left = (TextView) layout.findViewById(R.id.left); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); params.width = (int) maxLeftWidth; left.setLayoutParams(params); left.setText(key); TextView right = (TextView) layout.findViewById(R.id.right); right.setText(value); root.addView(layout); } private LinearLayout getItemLayout() { LinearLayout layout = (LinearLayout) LayoutInflater.from(this).inflate(R.layout.compose_item_layout, null); return layout; } }
改方案也需要先計算左邊的最大占用寬度,來設置右邊占用的大小,每一項的布局如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:paddingTop="@dimen/text_padding_10" tools:context=".activity.Typography1Activity"> <TextView android:id="@+id/left" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="@dimen/text_padding_10" android:textColor="@color/color_black_999999" android:textSize="@dimen/text_size_13"/> <TextView android:id="@+id/right" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:textColor="@color/black" android:textSize="@dimen/text_size_13"/> </LinearLayout>
每一行有兩個TextView,左邊寬度為自適應,右邊占據剩下左右的位置,在計算出左邊最大寬度後,重新設置左邊每一個TextView占用的寬度。
方案6
方式與1差不多,但是不在繼承TextView,而是直接繼承View:
public class TypographyView4 extends View { private Paint leftPaint = new Paint(); private Paint rightPaint = new Paint(); private int fullWidth; private float textSize; private JSONArray array; private int middlePadding = 0; float maxLeftWidth = 0; int itemSize = 0; public TypographyView4(Context context) { super(context); init(); } public TypographyView4(Context context, AttributeSet attrs) { super(context, attrs); init(); } public TypographyView4(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { textSize = getResources().getDimensionPixelSize(R.dimen.text_size_13); leftPaint.setAntiAlias(true); leftPaint.setTextSize(textSize); leftPaint.setColor(getResources().getColor(R.color.color_black_999999)); rightPaint.setAntiAlias(true); rightPaint.setTextSize(textSize); rightPaint.setColor(getResources().getColor(R.color.color_black)); middlePadding = getResources().getDimensionPixelSize(R.dimen.padding_value); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); fullWidth = getWidth();// 整個textView的寬度 } public void setText(JSONArray array) { this.array = array; if (array != null) { try { int size = itemSize = array.length(); for (int i = 0; i < size; ++i) { JSONArray o = (JSONArray) array.get(i); String key = o.getString(0); String value = o.getString(1); if (TextUtils.isEmpty(key) || TextUtils.isEmpty(value)) { itemSize--; continue; } float curWidth = leftPaint.measureText(key); if (curWidth > maxLeftWidth) { maxLeftWidth = curWidth; } } maxLeftWidth = maxLeftWidth + middlePadding; invalidate(); } catch (Exception e) { } } } @Override protected void onDraw(Canvas canvas) { if (array == null) { return; } int lineCount = 0; try { JSONArray item; float offsetY; for (int i = 0; i < itemSize; ++i) { item = (JSONArray) array.get(i); offsetY = (lineCount + 1) * textSize; canvas.drawText(item.getString(0), 0, offsetY, leftPaint); String value = item.getString(1); float valueWidth = rightPaint.measureText(value); if (valueWidth > fullWidth - maxLeftWidth) {// 一行顯示不完 char[] textCharArray = value.toCharArray(); float charWidth; float drawWidth = maxLeftWidth; for (int j = 0; j < textCharArray.length; j++) { charWidth = rightPaint.measureText(textCharArray, j, 1); if (fullWidth - drawWidth < charWidth) { lineCount++; drawWidth = maxLeftWidth; offsetY += textSize; } canvas.drawText(textCharArray, j, 1, drawWidth, offsetY, rightPaint); drawWidth += charWidth; } } else { canvas.drawText(value, maxLeftWidth, offsetY, rightPaint); } lineCount += 2; } } catch (JSONException e) { e.printStackTrace(); } } }
該方案主要繼承自View,不再繼承TextView,由於在在上述方案中不在調用super,因此TextView已經退化為一個View,因此直接繼承View。
總結
因為左邊的寬度不確定,因此所有的方案都進行了同樣的一個操作,就是測量了左邊顯示的最大寬度,後續的操作再根據該寬度進行調整。上述的方案中1,2,3,6都只需用一個View來進行顯示,4,5都需要多個View進行顯示。
完整的代碼可以在查看鏈接上進行查看。
以上就是本文的全部內容,希望對大家的學習有所幫助。
首先說一下canvas類:Class OverviewThe Canvas class holds the "draw" calls. To draw
手機能帶給你便捷、歡笑、感動,當然,也會帶給你小小的傷痛,比如,被偷、碎屏,或者掉進水裡——甭管這水是水池、下水道、馬桶還是火鍋,等
QQ5.0側滑效果實現方案有很多方式,今天我們使用ViewDragHelper來實現一下。先上效果圖: ①自定義控件SlidingMenu繼承FrameLayout,放在
概述: 我想我們在使用一些App的時候,應該不會出現一些“裸控件”的吧。除非是一些系統中的軟件,那是為了保持風格的一致性,做出的一些權衡。我這裡並