├── .gitignore ├── .idea ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── qiaomu │ │ └── tablerow │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── qiaomu │ │ │ └── tablerow │ │ │ ├── MainActivity.java │ │ │ ├── MyTabLayout.java │ │ │ ├── TableRowTextView.java │ │ │ ├── chart │ │ │ ├── Axis.java │ │ │ ├── BaseEntry.java │ │ │ ├── ChartConf.java │ │ │ ├── ChartView.java │ │ │ ├── Legend.java │ │ │ ├── PieAnimation.java │ │ │ ├── PieChart.java │ │ │ ├── PieEntry.java │ │ │ └── PiePosition.java │ │ │ └── view │ │ │ └── MyListView.java │ └── res │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_phone.xml │ │ └── content_main.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── qiaomu │ └── tablerow │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── 1.png ├── 2.png ├── 3.png └── 4.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # TableRowTextView表单,表格富媒体 3 | ### 添加依赖 4 | ```java 5 | dependencies{ 6 | compile 'com.qiaomu.library:tablerowview:1.0.2' 7 | } 8 | ``` 9 | 10 | ### 自适应单元格宽度模式 11 | ![image](https://github.com/mrme2014/TableRowTextView/raw/master/images/1.png) 12 | 13 | 14 | ### 固定单元格宽度模式,超过宽度自动换行,超过maxlines自动截断 15 | ![image](https://github.com/mrme2014/TableRowTextView/raw/master/images/2.png) 16 | 17 | ### 使用场景截图 18 | ![image](https://github.com/mrme2014/TableRowTextView/raw/master/images/3.png) 19 | 20 | ### 使用场景截图 21 | ![image](https://github.com/mrme2014/TableRowTextView/raw/master/images/4.png) 22 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.0" 6 | defaultConfig { 7 | applicationId "com.qiaomu.tablerow" 8 | minSdkVersion 15 9 | targetSdkVersion 23 10 | versionCode 1 11 | versionName "1.0" 12 | 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(include: ['*.jar'], dir: 'libs') 24 | compile 'com.android.support:appcompat-v7:25.1.1' 25 | compile 'com.android.support:design:25.1.1' 26 | compile 'com.android.support:support-v4:25.1.1' 27 | } 28 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Users\mrs\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/qiaomu/tablerow/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.qiaomu.tablerow; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.qiaomu.tablerow", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/qiaomu/tablerow/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.qiaomu.tablerow; 2 | 3 | import android.content.Context; 4 | import android.graphics.Color; 5 | import android.os.Bundle; 6 | import android.support.design.widget.FloatingActionButton; 7 | import android.support.design.widget.Snackbar; 8 | import android.support.design.widget.TabLayout; 9 | import android.support.v4.content.ContextCompat; 10 | import android.support.v7.app.AppCompatActivity; 11 | import android.support.v7.widget.Toolbar; 12 | import android.telephony.TelephonyManager; 13 | import android.text.SpannableString; 14 | import android.text.SpannableStringBuilder; 15 | import android.text.Spanned; 16 | import android.text.style.AbsoluteSizeSpan; 17 | import android.text.style.ForegroundColorSpan; 18 | import android.view.View; 19 | import android.view.Menu; 20 | import android.view.MenuItem; 21 | import android.widget.ArrayAdapter; 22 | import android.widget.ListView; 23 | import android.widget.Toast; 24 | 25 | import com.qiaomu.tablerow.chart.PieChart; 26 | import com.qiaomu.tablerow.chart.PieEntry; 27 | 28 | import java.util.ArrayList; 29 | import java.util.List; 30 | 31 | public class MainActivity extends AppCompatActivity { 32 | 33 | @Override 34 | protected void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | setContentView(R.layout.activity_main); 37 | TableRowTextView tableRow = (TableRowTextView) findViewById(R.id.tableRow); 38 | tableRow.setTextArray( 39 | getCharSequenceBuilder("主主胜主主主胜主", 0, 5, Color.RED), 40 | getCharSequenceBuilder("客胜", 0, 1, Color.RED), 41 | getCharSequenceBuilder("客胜主", 1, 2, Color.YELLOW), 42 | "胜", 43 | getCharSequenceBuilder("客胜主胜1234", 2, 5, Color.GREEN)); 44 | 45 | 46 | 47 | TableRowTextView tableRow1 = (TableRowTextView) findViewById(R.id.tableRow1); 48 | tableRow1.setTextArray( 49 | getCharSequenceBuilder("tableRow1", 0, 5, Color.RED), 50 | getCharSequenceBuilder("qiaomu", 0, 1, Color.RED), 51 | getCharSequenceBuilder("qiaomu乔木", 1, 2, Color.YELLOW), 52 | "吆吆吆", 53 | getCharSequenceBuilder("algin_cellMode_celldivider_rowdivider", 2, 5, Color.RED)); 54 | 55 | } 56 | 57 | private SpannableStringBuilder getCharSequenceBuilder(String host, int start, int end, int color) { 58 | SpannableStringBuilder ssb = new SpannableStringBuilder(host); 59 | ssb.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 60 | ssb.setSpan(new AbsoluteSizeSpan(color, false), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 61 | return ssb; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/qiaomu/tablerow/MyTabLayout.java: -------------------------------------------------------------------------------- 1 | package com.qiaomu.tablerow; 2 | 3 | /** 4 | * Created by mrs on 2017/4/21. 5 | */ 6 | 7 | /* 8 | * Copyright (C) 2015 The Android Open Source Project 9 | * 10 | * Licensed under the Apache License, Version 2.0 (the "License"); 11 | * you may not use this file except in compliance with the License. 12 | * You may obtain a copy of the License at 13 | * 14 | * http://www.apache.org/licenses/LICENSE-2.0 15 | * 16 | * Unless required by applicable law or agreed to in writing, software 17 | * distributed under the License is distributed on an "AS IS" BASIS, 18 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | * See the License for the specific language governing permissions and 20 | * limitations under the License. 21 | */ 22 | 23 | 24 | import android.animation.Animator; 25 | import android.animation.AnimatorListenerAdapter; 26 | import android.animation.ValueAnimator; 27 | import android.annotation.TargetApi; 28 | import android.content.Context; 29 | import android.content.res.ColorStateList; 30 | import android.content.res.Resources; 31 | import android.content.res.TypedArray; 32 | import android.graphics.Canvas; 33 | import android.graphics.Color; 34 | import android.graphics.Paint; 35 | import android.graphics.drawable.Drawable; 36 | import android.os.Build; 37 | import android.support.annotation.ColorInt; 38 | import android.support.annotation.DrawableRes; 39 | import android.support.annotation.IntDef; 40 | import android.support.annotation.LayoutRes; 41 | import android.support.annotation.NonNull; 42 | import android.support.annotation.Nullable; 43 | import android.support.annotation.StringRes; 44 | import android.support.v4.content.ContextCompat; 45 | import android.support.v4.view.GravityCompat; 46 | import android.support.v4.view.PagerAdapter; 47 | import android.support.v4.view.ViewCompat; 48 | import android.support.v4.view.ViewPager; 49 | import android.support.v4.view.animation.FastOutSlowInInterpolator; 50 | import android.support.v4.widget.TextViewCompat; 51 | import android.support.v7.app.ActionBar; 52 | import android.text.Layout; 53 | import android.text.TextUtils; 54 | import android.util.AttributeSet; 55 | import android.util.TypedValue; 56 | import android.view.Gravity; 57 | import android.view.LayoutInflater; 58 | import android.view.View; 59 | import android.view.ViewGroup; 60 | import android.view.ViewParent; 61 | import android.view.accessibility.AccessibilityEvent; 62 | import android.view.accessibility.AccessibilityNodeInfo; 63 | import android.widget.HorizontalScrollView; 64 | import android.widget.ImageView; 65 | import android.widget.LinearLayout; 66 | import android.widget.TextView; 67 | import android.widget.Toast; 68 | 69 | import java.lang.annotation.Retention; 70 | import java.lang.annotation.RetentionPolicy; 71 | import java.lang.ref.WeakReference; 72 | import java.util.ArrayList; 73 | import java.util.Iterator; 74 | 75 | /** 76 | * TabLayout provides a horizontal layout to display tabs. 77 | *

78 | *

Population of the tabs to display is 79 | * done through {@link Tab} instances. You create tabs via {@link #newTab()}. From there you can 80 | * change the tab's label or icon via {@link Tab#setText(int)} and {@link Tab#setIcon(int)} 81 | * respectively. To display the tab, you need to add it to the layout via one of the 82 | * {@link #addTab(Tab)} methods. For example: 83 | *

  84 |  * TabLayout tabLayout = ...;
  85 |  * tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
  86 |  * tabLayout.addTab(tabLayout.newTab().setText("Tab 2"));
  87 |  * tabLayout.addTab(tabLayout.newTab().setText("Tab 3"));
  88 |  * 
89 | * You should set a listener via {@link #setOnTabSelectedListener(OnTabSelectedListener)} to be 90 | * notified when any tab's selection state has been changed. 91 | *

92 | * If you're using a {@link android.support.v4.view.ViewPager} together 93 | * with this layout, you can use {@link #setTabsFromPagerAdapter(PagerAdapter)} which will populate 94 | * the tabs using the given {@link PagerAdapter}'s page titles. You should also use a 95 | * {@link TabLayoutOnPageChangeListener} to forward the scroll and selection changes to this 96 | * layout like so: 97 | *

  98 |  * ViewPager viewPager = ...;
  99 |  * TabLayout tabLayout = ...;
 100 |  * viewPager.addOnPageChangeListener(new TabLayoutOnPageChangeListener(tabLayout));
 101 |  * 
102 | * 103 | * @see Tabs 104 | */ 105 | public class MyTabLayout extends HorizontalScrollView { 106 | 107 | private static final int DEFAULT_HEIGHT_WITH_TEXT_ICON = 72; // dps 108 | private static final int DEFAULT_GAP_TEXT_ICON = 8; // dps 109 | private static final int INVALID_WIDTH = -1; 110 | private static final int DEFAULT_HEIGHT = 48; // dps 111 | private static final int TAB_MIN_WIDTH_MARGIN = 56; //dps 112 | private static final int FIXED_WRAP_GUTTER_MIN = 16; //dps 113 | private static final int MOTION_NON_ADJACENT_OFFSET = 24; 114 | 115 | private static final int ANIMATION_DURATION = 300; 116 | 117 | /** 118 | * Scrollable tabs display a subset of tabs at any given moment, and can contain longer tab 119 | * labels and a larger number of tabs. They are best used for browsing contexts in touch 120 | * interfaces when users don’t need to directly compare the tab labels. 121 | * 122 | * @see #setTabMode(int) 123 | * @see #getTabMode() 124 | */ 125 | public static final int MODE_SCROLLABLE = 0; 126 | 127 | /** 128 | * Fixed tabs display all tabs concurrently and are best used with content that benefits from 129 | * quick pivots between tabs. The maximum number of tabs is limited by the view’s width. 130 | * Fixed tabs have equal width, based on the widest tab label. 131 | * 132 | * @see #setTabMode(int) 133 | * @see #getTabMode() 134 | */ 135 | public static final int MODE_FIXED = 1; 136 | 137 | /** 138 | * @hide 139 | */ 140 | @IntDef(value = {MODE_SCROLLABLE, MODE_FIXED}) 141 | @Retention(RetentionPolicy.SOURCE) 142 | public @interface Mode { 143 | } 144 | 145 | /** 146 | * Gravity used to fill the {@link MyTabLayout} as much as possible. This option only takes effect 147 | * when used with {@link #MODE_FIXED}. 148 | * 149 | * @see #setTabGravity(int) 150 | * @see #getTabGravity() 151 | */ 152 | public static final int GRAVITY_FILL = 0; 153 | 154 | /** 155 | * Gravity used to lay out the tabs in the center of the {@link MyTabLayout}. 156 | * 157 | * @see #setTabGravity(int) 158 | * @see #getTabGravity() 159 | */ 160 | public static final int GRAVITY_CENTER = 1; 161 | 162 | /** 163 | * @hide 164 | */ 165 | @IntDef(flag = true, value = {GRAVITY_FILL, GRAVITY_CENTER}) 166 | @Retention(RetentionPolicy.SOURCE) 167 | public @interface TabGravity { 168 | } 169 | 170 | /** 171 | * Callback interface invoked when a tab's selection state changes. 172 | */ 173 | public interface OnTabSelectedListener { 174 | 175 | /** 176 | * Called when a tab enters the selected state. 177 | * 178 | * @param tab The tab that was selected 179 | */ 180 | public void onTabSelected(Tab tab); 181 | 182 | /** 183 | * Called when a tab exits the selected state. 184 | * 185 | * @param tab The tab that was unselected 186 | */ 187 | public void onTabUnselected(Tab tab); 188 | 189 | /** 190 | * Called when a tab that is already selected is chosen again by the user. Some applications 191 | * may use this action to return to the top level of a category. 192 | * 193 | * @param tab The tab that was reselected. 194 | */ 195 | public void onTabReselected(Tab tab); 196 | } 197 | 198 | private final ArrayList mTabs = new ArrayList<>(); 199 | private Tab mSelectedTab; 200 | 201 | private final SlidingTabStrip mTabStrip; 202 | 203 | private int mTabPaddingStart; 204 | private int mTabPaddingTop; 205 | private int mTabPaddingEnd; 206 | private int mTabPaddingBottom; 207 | 208 | private int mTabTextAppearance; 209 | private ColorStateList mTabTextColors; 210 | private float mTabTextSize; 211 | private float mTabTextMultiLineSize; 212 | 213 | private final int mTabBackgroundResId; 214 | 215 | private int mTabMaxWidth = Integer.MAX_VALUE; 216 | private final int mRequestedTabMinWidth; 217 | private final int mRequestedTabMaxWidth; 218 | private final int mScrollableTabMinWidth; 219 | 220 | private int mContentInsetStart; 221 | 222 | private int mTabGravity; 223 | private int mMode; 224 | 225 | private OnTabSelectedListener mOnTabSelectedListener; 226 | private View.OnClickListener mTabClickListener; 227 | 228 | private ValueAnimator mScrollAnimator; 229 | private ValueAnimator mIndicatorAnimator; 230 | 231 | public MyTabLayout(Context context) { 232 | this(context, null); 233 | } 234 | 235 | public MyTabLayout(Context context, AttributeSet attrs) { 236 | this(context, attrs, 0); 237 | } 238 | 239 | public MyTabLayout(Context context, AttributeSet attrs, int defStyleAttr) { 240 | super(context, attrs, defStyleAttr); 241 | 242 | // Disable the Scroll Bar 243 | setHorizontalScrollBarEnabled(false); 244 | 245 | // Add the TabStrip 246 | mTabStrip = new SlidingTabStrip(context); 247 | addView(mTabStrip, LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); 248 | 249 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout, 250 | defStyleAttr, R.style.Widget_Design_TabLayout); 251 | 252 | mTabStrip.setSelectedIndicatorHeight( 253 | a.getDimensionPixelSize(R.styleable.TabLayout_tabIndicatorHeight, 0)); 254 | mTabStrip.setSelectedIndicatorColor(a.getColor(R.styleable.TabLayout_tabIndicatorColor, 0)); 255 | 256 | mTabPaddingStart = mTabPaddingTop = mTabPaddingEnd = mTabPaddingBottom = a 257 | .getDimensionPixelSize(R.styleable.TabLayout_tabPadding, 0); 258 | mTabPaddingStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingStart, 259 | mTabPaddingStart); 260 | mTabPaddingTop = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingTop, 261 | mTabPaddingTop); 262 | mTabPaddingEnd = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingEnd, 263 | mTabPaddingEnd); 264 | mTabPaddingBottom = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingBottom, 265 | mTabPaddingBottom); 266 | 267 | mTabTextAppearance = a.getResourceId(R.styleable.TabLayout_tabTextAppearance, 268 | R.style.TextAppearance_Design_Tab); 269 | 270 | // Text colors/sizes come from the text appearance first 271 | final TypedArray ta = context.obtainStyledAttributes(mTabTextAppearance, 272 | R.styleable.TextAppearance); 273 | try { 274 | mTabTextSize = ta.getDimensionPixelSize(R.styleable.TextAppearance_android_textSize, 0); 275 | mTabTextColors = ta.getColorStateList(R.styleable.TextAppearance_android_textColor); 276 | } finally { 277 | ta.recycle(); 278 | } 279 | 280 | if (a.hasValue(R.styleable.TabLayout_tabTextColor)) { 281 | // If we have an explicit text color set, use it instead 282 | mTabTextColors = a.getColorStateList(R.styleable.TabLayout_tabTextColor); 283 | } 284 | 285 | if (a.hasValue(R.styleable.TabLayout_tabSelectedTextColor)) { 286 | // We have an explicit selected text color set, so we need to make merge it with the 287 | // current colors. This is exposed so that developers can use theme attributes to set 288 | // this (theme attrs in ColorStateLists are Lollipop+) 289 | final int selected = a.getColor(R.styleable.TabLayout_tabSelectedTextColor, 0); 290 | mTabTextColors = createColorStateList(mTabTextColors.getDefaultColor(), selected); 291 | } 292 | 293 | mRequestedTabMinWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMinWidth, 294 | INVALID_WIDTH); 295 | mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMaxWidth, 296 | INVALID_WIDTH); 297 | mTabBackgroundResId = a.getResourceId(R.styleable.TabLayout_tabBackground, 0); 298 | mContentInsetStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabContentStart, 0); 299 | mMode = a.getInt(R.styleable.TabLayout_tabMode, MODE_FIXED); 300 | mTabGravity = a.getInt(R.styleable.TabLayout_tabGravity, GRAVITY_FILL); 301 | a.recycle(); 302 | 303 | // TODO add attr for these 304 | final Resources res = getResources(); 305 | mTabTextMultiLineSize = res.getDimensionPixelSize(R.dimen.design_tab_text_size_2line); 306 | mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.design_tab_scrollable_min_width); 307 | 308 | // Now apply the tab mode and gravity 309 | applyModeAndGravity(); 310 | } 311 | 312 | /** 313 | * Sets the tab indicator's color for the currently selected tab. 314 | * 315 | * @param color color to use for the indicator 316 | */ 317 | public void setSelectedTabIndicatorColor(@ColorInt int color) { 318 | mTabStrip.setSelectedIndicatorColor(color); 319 | } 320 | 321 | /** 322 | * Sets the tab indicator's height for the currently selected tab. 323 | * 324 | * @param height height to use for the indicator in pixels 325 | */ 326 | public void setSelectedTabIndicatorHeight(int height) { 327 | mTabStrip.setSelectedIndicatorHeight(height); 328 | } 329 | 330 | /** 331 | * Set the scroll position of the tabs. This is useful for when the tabs are being displayed as 332 | * part of a scrolling container such as {@link android.support.v4.view.ViewPager}. 333 | *

334 | * Calling this method does not update the selected tab, it is only used for drawing purposes. 335 | * 336 | * @param position current scroll position 337 | * @param positionOffset Value from [0, 1) indicating the offset from {@code position}. 338 | * @param updateSelectedText Whether to update the text's selected state. 339 | */ 340 | public void setScrollPosition(int position, float positionOffset, boolean updateSelectedText) { 341 | if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { 342 | return; 343 | } 344 | if (position < 0 || position >= mTabStrip.getChildCount()) { 345 | return; 346 | } 347 | 348 | // Set the indicator position and update the scroll to match 349 | mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset); 350 | scrollTo(calculateScrollXForTab(position, positionOffset), 0); 351 | 352 | // Update the 'selected state' view as we scroll 353 | if (updateSelectedText) { 354 | setSelectedTabView(Math.round(position + positionOffset)); 355 | } 356 | } 357 | 358 | private float getScrollPosition() { 359 | return mTabStrip.getIndicatorPosition(); 360 | } 361 | 362 | /** 363 | * Add a tab to this layout. The tab will be added at the end of the list. 364 | * If this is the first tab to be added it will become the selected tab. 365 | * 366 | * @param tab Tab to add 367 | */ 368 | public void addTab(@NonNull Tab tab) { 369 | addTab(tab, mTabs.isEmpty()); 370 | } 371 | 372 | /** 373 | * Add a tab to this layout. The tab will be inserted at position. 374 | * If this is the first tab to be added it will become the selected tab. 375 | * 376 | * @param tab The tab to add 377 | * @param position The new position of the tab 378 | */ 379 | public void addTab(@NonNull Tab tab, int position) { 380 | addTab(tab, position, mTabs.isEmpty()); 381 | } 382 | 383 | /** 384 | * Add a tab to this layout. The tab will be added at the end of the list. 385 | * 386 | * @param tab Tab to add 387 | * @param setSelected True if the added tab should become the selected tab. 388 | */ 389 | public void addTab(@NonNull Tab tab, boolean setSelected) { 390 | if (tab.mParent != this) { 391 | throw new IllegalArgumentException("Tab belongs to a different TabLayout."); 392 | } 393 | 394 | addTabView(tab, setSelected); 395 | configureTab(tab, mTabs.size()); 396 | if (setSelected) { 397 | tab.select(); 398 | } 399 | } 400 | 401 | /** 402 | * Add a tab to this layout. The tab will be inserted at position. 403 | * 404 | * @param tab The tab to add 405 | * @param position The new position of the tab 406 | * @param setSelected True if the added tab should become the selected tab. 407 | */ 408 | public void addTab(@NonNull Tab tab, int position, boolean setSelected) { 409 | if (tab.mParent != this) { 410 | throw new IllegalArgumentException("Tab belongs to a different TabLayout."); 411 | } 412 | 413 | addTabView(tab, position, setSelected); 414 | configureTab(tab, position); 415 | if (setSelected) { 416 | tab.select(); 417 | } 418 | } 419 | 420 | /** 421 | * Set the {@link android.support.design.widget.TabLayout.OnTabSelectedListener} that will 422 | * handle switching to and from tabs. 423 | * 424 | * @param onTabSelectedListener Listener to handle tab selection events 425 | */ 426 | public void setOnTabSelectedListener(OnTabSelectedListener onTabSelectedListener) { 427 | mOnTabSelectedListener = onTabSelectedListener; 428 | } 429 | 430 | /** 431 | * Create and return a new {@link Tab}. You need to manually add this using 432 | * {@link #addTab(Tab)} or a related method. 433 | * 434 | * @return A new Tab 435 | * @see #addTab(Tab) 436 | */ 437 | @NonNull 438 | public Tab newTab() { 439 | return new Tab(this); 440 | } 441 | 442 | /** 443 | * Returns the number of tabs currently registered with the action bar. 444 | * 445 | * @return Tab count 446 | */ 447 | public int getTabCount() { 448 | return mTabs.size(); 449 | } 450 | 451 | /** 452 | * Returns the tab at the specified index. 453 | */ 454 | @Nullable 455 | public Tab getTabAt(int index) { 456 | return mTabs.get(index); 457 | } 458 | 459 | /** 460 | * Returns the position of the current selected tab. 461 | * 462 | * @return selected tab position, or {@code -1} if there isn't a selected tab. 463 | */ 464 | public int getSelectedTabPosition() { 465 | return mSelectedTab != null ? mSelectedTab.getPosition() : -1; 466 | } 467 | 468 | /** 469 | * Remove a tab from the layout. If the removed tab was selected it will be deselected 470 | * and another tab will be selected if present. 471 | * 472 | * @param tab The tab to remove 473 | */ 474 | public void removeTab(Tab tab) { 475 | if (tab.mParent != this) { 476 | throw new IllegalArgumentException("Tab does not belong to this TabLayout."); 477 | } 478 | 479 | removeTabAt(tab.getPosition()); 480 | } 481 | 482 | /** 483 | * Remove a tab from the layout. If the removed tab was selected it will be deselected 484 | * and another tab will be selected if present. 485 | * 486 | * @param position Position of the tab to remove 487 | */ 488 | public void removeTabAt(int position) { 489 | final int selectedTabPosition = mSelectedTab != null ? mSelectedTab.getPosition() : 0; 490 | removeTabViewAt(position); 491 | 492 | Tab removedTab = mTabs.remove(position); 493 | if (removedTab != null) { 494 | removedTab.setPosition(Tab.INVALID_POSITION); 495 | } 496 | 497 | final int newTabCount = mTabs.size(); 498 | for (int i = position; i < newTabCount; i++) { 499 | mTabs.get(i).setPosition(i); 500 | } 501 | 502 | if (selectedTabPosition == position) { 503 | selectTab(mTabs.isEmpty() ? null : mTabs.get(Math.max(0, position - 1))); 504 | } 505 | } 506 | 507 | /** 508 | * Remove all tabs from the action bar and deselect the current tab. 509 | */ 510 | public void removeAllTabs() { 511 | // Remove all the views 512 | mTabStrip.removeAllViews(); 513 | 514 | for (Iterator i = mTabs.iterator(); i.hasNext(); ) { 515 | Tab tab = i.next(); 516 | tab.setPosition(Tab.INVALID_POSITION); 517 | i.remove(); 518 | } 519 | 520 | mSelectedTab = null; 521 | } 522 | 523 | /** 524 | * Set the behavior mode for the Tabs in this layout. The valid input options are: 525 | *

533 | * 534 | * @param mode one of {@link #MODE_FIXED} or {@link #MODE_SCROLLABLE}. 535 | */ 536 | public void setTabMode(@Mode int mode) { 537 | if (mode != mMode) { 538 | mMode = mode; 539 | applyModeAndGravity(); 540 | } 541 | } 542 | 543 | /** 544 | * Returns the current mode used by this {@link MyTabLayout}. 545 | * 546 | * @see #setTabMode(int) 547 | */ 548 | @Mode 549 | public int getTabMode() { 550 | return mMode; 551 | } 552 | 553 | /** 554 | * Set the gravity to use when laying out the tabs. 555 | * 556 | * @param gravity one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}. 557 | */ 558 | public void setTabGravity(@TabGravity int gravity) { 559 | if (mTabGravity != gravity) { 560 | mTabGravity = gravity; 561 | applyModeAndGravity(); 562 | } 563 | } 564 | 565 | /** 566 | * The current gravity used for laying out tabs. 567 | * 568 | * @return one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}. 569 | */ 570 | @TabGravity 571 | public int getTabGravity() { 572 | return mTabGravity; 573 | } 574 | 575 | /** 576 | * Sets the text colors for the different states (normal, selected) used for the tabs. 577 | */ 578 | public void setTabTextColors(@Nullable ColorStateList textColor) { 579 | if (mTabTextColors != textColor) { 580 | mTabTextColors = textColor; 581 | updateAllTabs(); 582 | } 583 | } 584 | 585 | /** 586 | * Gets the text colors for the different states (normal, selected) used for the tabs. 587 | */ 588 | @Nullable 589 | public ColorStateList getTabTextColors() { 590 | return mTabTextColors; 591 | } 592 | 593 | /** 594 | * Sets the text colors for the different states (normal, selected) used for the tabs. 595 | */ 596 | public void setTabTextColors(int normalColor, int selectedColor) { 597 | setTabTextColors(createColorStateList(normalColor, selectedColor)); 598 | } 599 | 600 | /** 601 | * The one-stop shop for setting up this {@link MyTabLayout} with a {@link ViewPager}. 602 | *

603 | *

This method will: 604 | *

611 | *

612 | * 613 | * @see #setTabsFromPagerAdapter(PagerAdapter) 614 | * @see TabLayoutOnPageChangeListener 615 | * @see ViewPagerOnTabSelectedListener 616 | */ 617 | public MyTabLayout setupWithViewPager(@NonNull ViewPager viewPager) { 618 | final PagerAdapter adapter = viewPager.getAdapter(); 619 | if (adapter == null) { 620 | throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set"); 621 | } 622 | 623 | // First we'll add Tabs, using the adapter's page titles 624 | setTabsFromPagerAdapter(adapter); 625 | 626 | // Now we'll add our page change listener to the ViewPager 627 | viewPager.addOnPageChangeListener(new TabLayoutOnPageChangeListener(this)); 628 | 629 | // Now we'll add a tab selected listener to set ViewPager's current item 630 | setOnTabSelectedListener(new ViewPagerOnTabSelectedListener(viewPager)); 631 | 632 | // Make sure we reflect the currently set ViewPager item 633 | if (adapter.getCount() > 0) { 634 | final int curItem = viewPager.getCurrentItem(); 635 | if (getSelectedTabPosition() != curItem) { 636 | selectTab(getTabAt(curItem)); 637 | } 638 | } 639 | return this; 640 | } 641 | 642 | /** 643 | * Populate our tab content from the given {@link PagerAdapter}. 644 | *

645 | * Any existing tabs will be removed first. Each tab will have it's text set to the value 646 | * returned from {@link PagerAdapter#getPageTitle(int)} 647 | *

648 | * 649 | * @param adapter the adapter to populate from 650 | */ 651 | public MyTabLayout setTabsFromPagerAdapter(@NonNull PagerAdapter adapter) { 652 | removeAllTabs(); 653 | for (int i = 0, count = adapter.getCount(); i < count; i++) { 654 | addTab(newTab().setText(adapter.getPageTitle(i))); 655 | } 656 | return this; 657 | } 658 | 659 | private void updateAllTabs() { 660 | for (int i = 0, z = mTabStrip.getChildCount(); i < z; i++) { 661 | updateTab(i); 662 | } 663 | } 664 | 665 | private TabView createTabView(Tab tab) { 666 | final TabView tabView = new TabView(getContext(), tab); 667 | tabView.setFocusable(true); 668 | tabView.setMinimumWidth(getTabMinWidth()); 669 | 670 | if (mTabClickListener == null) { 671 | mTabClickListener = new View.OnClickListener() { 672 | @Override 673 | public void onClick(View view) { 674 | TabView tabView = (TabView) view; 675 | tabView.getTab().select(); 676 | } 677 | }; 678 | } 679 | tabView.setOnClickListener(mTabClickListener); 680 | return tabView; 681 | } 682 | 683 | private void configureTab(Tab tab, int position) { 684 | tab.setPosition(position); 685 | mTabs.add(position, tab); 686 | 687 | final int count = mTabs.size(); 688 | for (int i = position + 1; i < count; i++) { 689 | mTabs.get(i).setPosition(i); 690 | } 691 | } 692 | 693 | private void updateTab(int position) { 694 | final TabView view = getTabView(position); 695 | if (view != null) { 696 | view.update(); 697 | } 698 | } 699 | 700 | private TabView getTabView(int position) { 701 | return (TabView) mTabStrip.getChildAt(position); 702 | } 703 | 704 | private void addTabView(Tab tab, boolean setSelected) { 705 | final TabView tabView = createTabView(tab); 706 | mTabStrip.addView(tabView, createLayoutParamsForTabs()); 707 | if (setSelected) { 708 | tabView.setSelected(true); 709 | } 710 | } 711 | 712 | private void addTabView(Tab tab, int position, boolean setSelected) { 713 | final TabView tabView = createTabView(tab); 714 | mTabStrip.addView(tabView, position, createLayoutParamsForTabs()); 715 | if (setSelected) { 716 | tabView.setSelected(true); 717 | } 718 | } 719 | 720 | private LinearLayout.LayoutParams createLayoutParamsForTabs() { 721 | final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( 722 | LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); 723 | updateTabViewLayoutParams(lp); 724 | return lp; 725 | } 726 | 727 | private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) { 728 | if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) { 729 | lp.width = 0; 730 | lp.weight = 1; 731 | } else { 732 | lp.width = LinearLayout.LayoutParams.WRAP_CONTENT; 733 | lp.weight = 0; 734 | } 735 | } 736 | 737 | private int dpToPx(int dps) { 738 | return Math.round(getResources().getDisplayMetrics().density * dps); 739 | } 740 | 741 | @Override 742 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 743 | // If we have a MeasureSpec which allows us to decide our height, try and use the default 744 | // height 745 | final int idealHeight = dpToPx(getDefaultHeight()) + getPaddingTop() + getPaddingBottom(); 746 | switch (MeasureSpec.getMode(heightMeasureSpec)) { 747 | case MeasureSpec.AT_MOST: 748 | heightMeasureSpec = MeasureSpec.makeMeasureSpec( 749 | Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)), 750 | MeasureSpec.EXACTLY); 751 | break; 752 | case MeasureSpec.UNSPECIFIED: 753 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY); 754 | break; 755 | } 756 | 757 | final int specWidth = MeasureSpec.getSize(widthMeasureSpec); 758 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) { 759 | // If we don't have an unspecified width spec, use the given size to calculate 760 | // the max tab width 761 | mTabMaxWidth = mRequestedTabMaxWidth > 0 762 | ? mRequestedTabMaxWidth 763 | : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN); 764 | } 765 | 766 | // Now super measure itself using the (possibly) modified height spec 767 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 768 | 769 | if (getChildCount() == 1) { 770 | // If we're in fixed mode then we need to make the tab strip is the same width as us 771 | // so we don't scroll 772 | final View child = getChildAt(0); 773 | boolean remeasure = false; 774 | 775 | switch (mMode) { 776 | case MODE_SCROLLABLE: 777 | // We only need to resize the child if it's smaller than us. This is similar 778 | // to fillViewport 779 | remeasure = child.getMeasuredWidth() < getMeasuredWidth(); 780 | break; 781 | case MODE_FIXED: 782 | // Resize the child so that it doesn't scroll 783 | remeasure = child.getMeasuredWidth() != getMeasuredWidth(); 784 | break; 785 | } 786 | 787 | if (remeasure) { 788 | // Re-measure the child with a widthSpec set to be exactly our measure width 789 | int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() 790 | + getPaddingBottom(), child.getLayoutParams().height); 791 | int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( 792 | getMeasuredWidth(), MeasureSpec.EXACTLY); 793 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 794 | } 795 | } 796 | } 797 | 798 | private void removeTabViewAt(int position) { 799 | mTabStrip.removeViewAt(position); 800 | requestLayout(); 801 | } 802 | 803 | private void animateToTab(int newPosition) { 804 | if (newPosition == Tab.INVALID_POSITION) { 805 | return; 806 | } 807 | 808 | if (getWindowToken() == null || !ViewCompat.isLaidOut(this) 809 | || mTabStrip.childrenNeedLayout()) { 810 | // If we don't have a window token, or we haven't been laid out yet just draw the new 811 | // position now 812 | setScrollPosition(newPosition, 0f, true); 813 | return; 814 | } 815 | 816 | final int startScrollX = getScrollX(); 817 | final int targetScrollX = calculateScrollXForTab(newPosition, 0); 818 | 819 | if (startScrollX != targetScrollX) { 820 | if (mScrollAnimator == null) { 821 | mScrollAnimator = new ValueAnimator(); 822 | mScrollAnimator.setInterpolator(new FastOutSlowInInterpolator()); 823 | mScrollAnimator.setDuration(ANIMATION_DURATION); 824 | mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 825 | @Override 826 | public void onAnimationUpdate(ValueAnimator animator) { 827 | scrollTo((int) animator.getAnimatedValue(), 0); 828 | } 829 | }); 830 | } 831 | 832 | mScrollAnimator.setIntValues(startScrollX, targetScrollX); 833 | mScrollAnimator.start(); 834 | } 835 | 836 | // Now animate the indicator 837 | mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION); 838 | } 839 | 840 | private void setSelectedTabView(int position) { 841 | final int tabCount = mTabStrip.getChildCount(); 842 | if (position < tabCount && !mTabStrip.getChildAt(position).isSelected()) { 843 | for (int i = 0; i < tabCount; i++) { 844 | final View child = mTabStrip.getChildAt(i); 845 | child.setSelected(i == position); 846 | } 847 | } 848 | } 849 | 850 | void selectTab(Tab tab) { 851 | selectTab(tab, true); 852 | } 853 | 854 | void selectTab(Tab tab, boolean updateIndicator) { 855 | if (mSelectedTab == tab) { 856 | if (mSelectedTab != null) { 857 | if (mOnTabSelectedListener != null) { 858 | mOnTabSelectedListener.onTabReselected(mSelectedTab); 859 | } 860 | animateToTab(tab.getPosition()); 861 | } 862 | } else { 863 | if (updateIndicator) { 864 | final int newPosition = tab != null ? tab.getPosition() : Tab.INVALID_POSITION; 865 | if (newPosition != Tab.INVALID_POSITION) { 866 | setSelectedTabView(newPosition); 867 | } 868 | if ((mSelectedTab == null || mSelectedTab.getPosition() == Tab.INVALID_POSITION) 869 | && newPosition != Tab.INVALID_POSITION) { 870 | // If we don't currently have a tab, just draw the indicator 871 | setScrollPosition(newPosition, 0f, true); 872 | } else { 873 | animateToTab(newPosition); 874 | } 875 | } 876 | if (mSelectedTab != null && mOnTabSelectedListener != null) { 877 | mOnTabSelectedListener.onTabUnselected(mSelectedTab); 878 | } 879 | mSelectedTab = tab; 880 | if (mSelectedTab != null && mOnTabSelectedListener != null) { 881 | mOnTabSelectedListener.onTabSelected(mSelectedTab); 882 | } 883 | } 884 | } 885 | 886 | private int calculateScrollXForTab(int position, float positionOffset) { 887 | if (mMode == MODE_SCROLLABLE) { 888 | final View selectedChild = mTabStrip.getChildAt(position); 889 | final View nextChild = position + 1 < mTabStrip.getChildCount() 890 | ? mTabStrip.getChildAt(position + 1) 891 | : null; 892 | final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0; 893 | final int nextWidth = nextChild != null ? nextChild.getWidth() : 0; 894 | 895 | return selectedChild.getLeft() 896 | + ((int) ((selectedWidth + nextWidth) * positionOffset * 0.5f)) 897 | + (selectedChild.getWidth() / 2) 898 | - (getWidth() / 2); 899 | } 900 | return 0; 901 | } 902 | 903 | private void applyModeAndGravity() { 904 | int paddingStart = 0; 905 | if (mMode == MODE_SCROLLABLE) { 906 | // If we're scrollable, or fixed at start, inset using padding 907 | paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart); 908 | } 909 | ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0); 910 | 911 | switch (mMode) { 912 | case MODE_FIXED: 913 | mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL); 914 | break; 915 | case MODE_SCROLLABLE: 916 | mTabStrip.setGravity(GravityCompat.START); 917 | break; 918 | } 919 | 920 | updateTabViews(true); 921 | } 922 | 923 | private void updateTabViews(final boolean requestLayout) { 924 | for (int i = 0; i < mTabStrip.getChildCount(); i++) { 925 | View child = mTabStrip.getChildAt(i); 926 | child.setMinimumWidth(getTabMinWidth()); 927 | updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams()); 928 | if (requestLayout) { 929 | child.requestLayout(); 930 | } 931 | } 932 | } 933 | 934 | /** 935 | * A tab in this layout. Instances can be created via {@link #newTab()}. 936 | */ 937 | public static final class Tab { 938 | 939 | /** 940 | * An invalid position for a tab. 941 | * 942 | * @see #getPosition() 943 | */ 944 | public static final int INVALID_POSITION = -1; 945 | 946 | private Object mTag; 947 | private Drawable mIcon; 948 | private CharSequence mText; 949 | private CharSequence mContentDesc; 950 | private int mPosition = INVALID_POSITION; 951 | private View mCustomView; 952 | 953 | private final MyTabLayout mParent; 954 | 955 | Tab(MyTabLayout parent) { 956 | mParent = parent; 957 | } 958 | 959 | /** 960 | * @return This Tab's tag object. 961 | */ 962 | @Nullable 963 | public Object getTag() { 964 | return mTag; 965 | } 966 | 967 | /** 968 | * Give this Tab an arbitrary object to hold for later use. 969 | * 970 | * @param tag Object to store 971 | * @return The current instance for call chaining 972 | */ 973 | @NonNull 974 | public Tab setTag(@Nullable Object tag) { 975 | mTag = tag; 976 | return this; 977 | } 978 | 979 | 980 | /** 981 | * Returns the custom view used for this tab. 982 | * 983 | * @see #setCustomView(View) 984 | * @see #setCustomView(int) 985 | */ 986 | @Nullable 987 | public View getCustomView() { 988 | return mCustomView; 989 | } 990 | 991 | /** 992 | * Set a custom view to be used for this tab. 993 | *

994 | * If the provided view contains a {@link TextView} with an ID of 995 | * {@link android.R.id#text1} then that will be updated with the value given 996 | * to {@link #setText(CharSequence)}. Similarly, if this layout contains an 997 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with 998 | * the value given to {@link #setIcon(Drawable)}. 999 | *

1000 | * 1001 | * @param view Custom view to be used as a tab. 1002 | * @return The current instance for call chaining 1003 | */ 1004 | @NonNull 1005 | public Tab setCustomView(@Nullable View view) { 1006 | mCustomView = view; 1007 | if (mPosition >= 0) { 1008 | mParent.updateTab(mPosition); 1009 | } 1010 | return this; 1011 | } 1012 | 1013 | /** 1014 | * Set a custom view to be used for this tab. 1015 | *

1016 | * If the inflated layout contains a {@link TextView} with an ID of 1017 | * {@link android.R.id#text1} then that will be updated with the value given 1018 | * to {@link #setText(CharSequence)}. Similarly, if this layout contains an 1019 | * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with 1020 | * the value given to {@link #setIcon(Drawable)}. 1021 | *

1022 | * 1023 | * @param resId A layout resource to inflate and use as a custom tab view 1024 | * @return The current instance for call chaining 1025 | */ 1026 | @NonNull 1027 | public Tab setCustomView(@LayoutRes int resId) { 1028 | final TabView tabView = mParent.getTabView(mPosition); 1029 | final LayoutInflater inflater = LayoutInflater.from(tabView.getContext()); 1030 | return setCustomView(inflater.inflate(resId, tabView, false)); 1031 | } 1032 | 1033 | /** 1034 | * Return the icon associated with this tab. 1035 | * 1036 | * @return The tab's icon 1037 | */ 1038 | @Nullable 1039 | public Drawable getIcon() { 1040 | return mIcon; 1041 | } 1042 | 1043 | /** 1044 | * Return the current position of this tab in the action bar. 1045 | * 1046 | * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in 1047 | * the action bar. 1048 | */ 1049 | public int getPosition() { 1050 | return mPosition; 1051 | } 1052 | 1053 | void setPosition(int position) { 1054 | mPosition = position; 1055 | } 1056 | 1057 | /** 1058 | * Return the text of this tab. 1059 | * 1060 | * @return The tab's text 1061 | */ 1062 | @Nullable 1063 | public CharSequence getText() { 1064 | return mText; 1065 | } 1066 | 1067 | /** 1068 | * Set the icon displayed on this tab. 1069 | * 1070 | * @param icon The drawable to use as an icon 1071 | * @return The current instance for call chaining 1072 | */ 1073 | @NonNull 1074 | public Tab setIcon(@Nullable Drawable icon) { 1075 | mIcon = icon; 1076 | if (mPosition >= 0) { 1077 | mParent.updateTab(mPosition); 1078 | } 1079 | return this; 1080 | } 1081 | 1082 | /** 1083 | * Set the icon displayed on this tab. 1084 | * 1085 | * @param resId A resource ID referring to the icon that should be displayed 1086 | * @return The current instance for call chaining 1087 | */ 1088 | @NonNull 1089 | public Tab setIcon(@DrawableRes int resId) { 1090 | return setIcon(ContextCompat.getDrawable(mParent.getContext(), resId)); 1091 | } 1092 | 1093 | /** 1094 | * Set the text displayed on this tab. Text may be truncated if there is not room to display 1095 | * the entire string. 1096 | * 1097 | * @param text The text to display 1098 | * @return The current instance for call chaining 1099 | */ 1100 | @NonNull 1101 | public Tab setText(@Nullable CharSequence text) { 1102 | mText = text; 1103 | if (mPosition >= 0) { 1104 | mParent.updateTab(mPosition); 1105 | } 1106 | return this; 1107 | } 1108 | 1109 | /** 1110 | * Set the text displayed on this tab. Text may be truncated if there is not room to display 1111 | * the entire string. 1112 | * 1113 | * @param resId A resource ID referring to the text that should be displayed 1114 | * @return The current instance for call chaining 1115 | */ 1116 | @NonNull 1117 | public Tab setText(@StringRes int resId) { 1118 | return setText(mParent.getResources().getText(resId)); 1119 | } 1120 | 1121 | /** 1122 | * Select this tab. Only valid if the tab has been added to the action bar. 1123 | */ 1124 | public void select() { 1125 | mParent.selectTab(this); 1126 | } 1127 | 1128 | /** 1129 | * Returns true if this tab is currently selected. 1130 | */ 1131 | public boolean isSelected() { 1132 | return mParent.getSelectedTabPosition() == mPosition; 1133 | } 1134 | 1135 | /** 1136 | * Set a description of this tab's content for use in accessibility support. If no content 1137 | * description is provided the Title will be used. 1138 | * 1139 | * @param resId A resource ID referring to the description text 1140 | * @return The current instance for call chaining 1141 | * @see #setContentDescription(CharSequence) 1142 | * @see #getContentDescription() 1143 | */ 1144 | @NonNull 1145 | public Tab setContentDescription(@StringRes int resId) { 1146 | return setContentDescription(mParent.getResources().getText(resId)); 1147 | } 1148 | 1149 | /** 1150 | * Set a description of this tab's content for use in accessibility support. If no content 1151 | * description is provided the Title will be used. 1152 | * 1153 | * @param contentDesc Description of this tab's content 1154 | * @return The current instance for call chaining 1155 | * @see #setContentDescription(int) 1156 | * @see #getContentDescription() 1157 | */ 1158 | @NonNull 1159 | public Tab setContentDescription(@Nullable CharSequence contentDesc) { 1160 | mContentDesc = contentDesc; 1161 | if (mPosition >= 0) { 1162 | mParent.updateTab(mPosition); 1163 | } 1164 | return this; 1165 | } 1166 | 1167 | /** 1168 | * Gets a brief description of this tab's content for use in accessibility support. 1169 | * 1170 | * @return Description of this tab's content 1171 | * @see #setContentDescription(CharSequence) 1172 | * @see #setContentDescription(int) 1173 | */ 1174 | @Nullable 1175 | public CharSequence getContentDescription() { 1176 | return mContentDesc; 1177 | } 1178 | } 1179 | 1180 | class TabView extends LinearLayout implements OnLongClickListener { 1181 | private final Tab mTab; 1182 | private TextView mTextView; 1183 | private ImageView mIconView; 1184 | 1185 | private View mCustomView; 1186 | private TextView mCustomTextView; 1187 | private ImageView mCustomIconView; 1188 | 1189 | private int mDefaultMaxLines = 2; 1190 | 1191 | public TabView(Context context, Tab tab) { 1192 | super(context); 1193 | mTab = tab; 1194 | if (mTabBackgroundResId != 0) { 1195 | setBackgroundDrawable(ContextCompat.getDrawable(context, mTabBackgroundResId)); 1196 | } 1197 | ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop, 1198 | mTabPaddingEnd, mTabPaddingBottom); 1199 | setGravity(Gravity.CENTER); 1200 | setOrientation(VERTICAL); 1201 | update(); 1202 | } 1203 | 1204 | @Override 1205 | public void setSelected(boolean selected) { 1206 | final boolean changed = (isSelected() != selected); 1207 | super.setSelected(selected); 1208 | if (changed && selected) { 1209 | sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 1210 | 1211 | if (mTextView != null) { 1212 | mTextView.setSelected(selected); 1213 | } 1214 | if (mIconView != null) { 1215 | mIconView.setSelected(selected); 1216 | } 1217 | } 1218 | } 1219 | 1220 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 1221 | @Override 1222 | public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1223 | super.onInitializeAccessibilityEvent(event); 1224 | // This view masquerades as an action bar tab. 1225 | event.setClassName(ActionBar.Tab.class.getName()); 1226 | } 1227 | 1228 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 1229 | @Override 1230 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1231 | super.onInitializeAccessibilityNodeInfo(info); 1232 | // This view masquerades as an action bar tab. 1233 | info.setClassName(ActionBar.Tab.class.getName()); 1234 | } 1235 | 1236 | @Override 1237 | public void onMeasure(final int origWidthMeasureSpec, final int origHeightMeasureSpec) { 1238 | final int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec); 1239 | final int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec); 1240 | final int maxWidth = getTabMaxWidth(); 1241 | 1242 | final int widthMeasureSpec; 1243 | final int heightMeasureSpec = origHeightMeasureSpec; 1244 | 1245 | if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED 1246 | || specWidthSize > maxWidth)) { 1247 | // If we have a max width and a given spec which is either unspecified or 1248 | // larger than the max width, update the width spec using the same mode 1249 | widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTabMaxWidth, specWidthMode); 1250 | } else { 1251 | // Else, use the original width spec 1252 | widthMeasureSpec = origWidthMeasureSpec; 1253 | } 1254 | 1255 | // Now lets measure 1256 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1257 | 1258 | // We need to switch the text size based on whether the text is spanning 2 lines or not 1259 | if (mTextView != null) { 1260 | final Resources res = getResources(); 1261 | float textSize = mTabTextSize; 1262 | int maxLines = mDefaultMaxLines; 1263 | 1264 | if (mIconView != null && mIconView.getVisibility() == VISIBLE) { 1265 | // If the icon view is being displayed, we limit the text to 1 line 1266 | maxLines = 1; 1267 | } else if (mTextView != null && mTextView.getLineCount() > 1) { 1268 | // Otherwise when we have text which wraps we reduce the text size 1269 | textSize = mTabTextMultiLineSize; 1270 | } 1271 | 1272 | final float curTextSize = mTextView.getTextSize(); 1273 | final int curLineCount = mTextView.getLineCount(); 1274 | final int curMaxLines = TextViewCompat.getMaxLines(mTextView); 1275 | 1276 | if (textSize != curTextSize || (curMaxLines >= 0 && maxLines != curMaxLines)) { 1277 | // We've got a new text size and/or max lines... 1278 | boolean updateTextView = true; 1279 | 1280 | if (mMode == MODE_FIXED && textSize > curTextSize && curLineCount == 1) { 1281 | // If we're in fixed mode, going up in text size and currently have 1 line 1282 | // then it's very easy to get into an infinite recursion. 1283 | // To combat that we check to see if the change in text size 1284 | // will cause a line count change. If so, abort the size change. 1285 | final Layout layout = mTextView.getLayout(); 1286 | if (layout == null 1287 | || approximateLineWidth(layout, 0, textSize) > layout.getWidth()) { 1288 | updateTextView = false; 1289 | } 1290 | } 1291 | 1292 | if (updateTextView) { 1293 | mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); 1294 | mTextView.setMaxLines(maxLines); 1295 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1296 | } 1297 | } 1298 | } 1299 | } 1300 | 1301 | final void update() { 1302 | final Tab tab = mTab; 1303 | final View custom = tab.getCustomView(); 1304 | if (custom != null) { 1305 | final ViewParent customParent = custom.getParent(); 1306 | if (customParent != this) { 1307 | if (customParent != null) { 1308 | ((ViewGroup) customParent).removeView(custom); 1309 | } 1310 | addView(custom); 1311 | } 1312 | mCustomView = custom; 1313 | if (mTextView != null) { 1314 | mTextView.setVisibility(GONE); 1315 | } 1316 | if (mIconView != null) { 1317 | mIconView.setVisibility(GONE); 1318 | mIconView.setImageDrawable(null); 1319 | } 1320 | 1321 | mCustomTextView = (TextView) custom.findViewById(android.R.id.text1); 1322 | if (mCustomTextView != null) { 1323 | mDefaultMaxLines = TextViewCompat.getMaxLines(mCustomTextView); 1324 | } 1325 | mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon); 1326 | } else { 1327 | // We do not have a custom view. Remove one if it already exists 1328 | if (mCustomView != null) { 1329 | removeView(mCustomView); 1330 | mCustomView = null; 1331 | } 1332 | mCustomTextView = null; 1333 | mCustomIconView = null; 1334 | } 1335 | 1336 | if (mCustomView == null) { 1337 | // If there isn't a custom view, we'll us our own in-built layouts 1338 | if (mIconView == null) { 1339 | ImageView iconView = (ImageView) LayoutInflater.from(getContext()) 1340 | .inflate(R.layout.design_layout_tab_icon, this, false); 1341 | addView(iconView, 0); 1342 | mIconView = iconView; 1343 | } 1344 | if (mTextView == null) { 1345 | TextView textView = (TextView) LayoutInflater.from(getContext()) 1346 | .inflate(R.layout.design_layout_tab_text, this, false); 1347 | addView(textView); 1348 | mTextView = textView; 1349 | mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView); 1350 | } 1351 | mTextView.setTextAppearance(getContext(), mTabTextAppearance); 1352 | if (mTabTextColors != null) { 1353 | mTextView.setTextColor(mTabTextColors); 1354 | } 1355 | updateTextAndIcon(tab, mTextView, mIconView); 1356 | } else { 1357 | // Else, we'll see if there is a TextView or ImageView present and update them 1358 | if (mCustomTextView != null || mCustomIconView != null) { 1359 | updateTextAndIcon(tab, mCustomTextView, mCustomIconView); 1360 | } 1361 | } 1362 | } 1363 | 1364 | private void updateTextAndIcon(Tab tab, TextView textView, ImageView iconView) { 1365 | final Drawable icon = tab.getIcon(); 1366 | final CharSequence text = tab.getText(); 1367 | 1368 | if (iconView != null) { 1369 | if (icon != null) { 1370 | iconView.setImageDrawable(icon); 1371 | iconView.setVisibility(VISIBLE); 1372 | setVisibility(VISIBLE); 1373 | } else { 1374 | iconView.setVisibility(GONE); 1375 | iconView.setImageDrawable(null); 1376 | } 1377 | iconView.setContentDescription(tab.getContentDescription()); 1378 | } 1379 | 1380 | final boolean hasText = !TextUtils.isEmpty(text); 1381 | if (textView != null) { 1382 | if (hasText) { 1383 | textView.setText(text); 1384 | textView.setContentDescription(tab.getContentDescription()); 1385 | textView.setVisibility(VISIBLE); 1386 | setVisibility(VISIBLE); 1387 | } else { 1388 | textView.setVisibility(GONE); 1389 | textView.setText(null); 1390 | } 1391 | } 1392 | 1393 | if (iconView != null) { 1394 | MarginLayoutParams lp = ((MarginLayoutParams) iconView.getLayoutParams()); 1395 | int bottomMargin = 0; 1396 | if (hasText && iconView.getVisibility() == VISIBLE) { 1397 | // If we're showing both text and icon, add some margin bottom to the icon 1398 | bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON); 1399 | } 1400 | if (bottomMargin != lp.bottomMargin) { 1401 | lp.bottomMargin = bottomMargin; 1402 | iconView.requestLayout(); 1403 | } 1404 | } 1405 | 1406 | if (!hasText && !TextUtils.isEmpty(tab.getContentDescription())) { 1407 | setOnLongClickListener(this); 1408 | } else { 1409 | setOnLongClickListener(null); 1410 | setLongClickable(false); 1411 | } 1412 | } 1413 | 1414 | @Override 1415 | public boolean onLongClick(View v) { 1416 | final int[] screenPos = new int[2]; 1417 | getLocationOnScreen(screenPos); 1418 | 1419 | final Context context = getContext(); 1420 | final int width = getWidth(); 1421 | final int height = getHeight(); 1422 | final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; 1423 | 1424 | Toast cheatSheet = Toast.makeText(context, mTab.getContentDescription(), 1425 | Toast.LENGTH_SHORT); 1426 | // Show under the tab 1427 | cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, 1428 | (screenPos[0] + width / 2) - screenWidth / 2, height); 1429 | 1430 | cheatSheet.show(); 1431 | return true; 1432 | } 1433 | 1434 | public Tab getTab() { 1435 | return mTab; 1436 | } 1437 | 1438 | /** 1439 | * Approximates a given lines width with the new provided text size. 1440 | */ 1441 | private float approximateLineWidth(Layout layout, int line, float textSize) { 1442 | return layout.getLineWidth(line) * (textSize / layout.getPaint().getTextSize()); 1443 | } 1444 | } 1445 | 1446 | private class SlidingTabStrip extends LinearLayout { 1447 | private int mSelectedIndicatorHeight; 1448 | private final Paint mSelectedIndicatorPaint; 1449 | 1450 | private int mSelectedPosition = -1; 1451 | private float mSelectionOffset; 1452 | 1453 | private int mIndicatorLeft = -1; 1454 | private int mIndicatorRight = -1; 1455 | 1456 | private ValueAnimator mCurrentAnimator; 1457 | 1458 | SlidingTabStrip(Context context) { 1459 | super(context); 1460 | setWillNotDraw(false); 1461 | mSelectedIndicatorPaint = new Paint(); 1462 | } 1463 | 1464 | void setSelectedIndicatorColor(int color) { 1465 | if (mSelectedIndicatorPaint.getColor() != color) { 1466 | mSelectedIndicatorPaint.setColor(color); 1467 | ViewCompat.postInvalidateOnAnimation(this); 1468 | } 1469 | } 1470 | 1471 | void setSelectedIndicatorHeight(int height) { 1472 | if (mSelectedIndicatorHeight != height) { 1473 | mSelectedIndicatorHeight = height; 1474 | ViewCompat.postInvalidateOnAnimation(this); 1475 | } 1476 | } 1477 | 1478 | boolean childrenNeedLayout() { 1479 | for (int i = 0, z = getChildCount(); i < z; i++) { 1480 | final View child = getChildAt(i); 1481 | if (child.getWidth() <= 0) { 1482 | return true; 1483 | } 1484 | } 1485 | return false; 1486 | } 1487 | 1488 | void setIndicatorPositionFromTabPosition(int position, float positionOffset) { 1489 | mSelectedPosition = position; 1490 | mSelectionOffset = positionOffset; 1491 | updateIndicatorPosition(); 1492 | } 1493 | 1494 | float getIndicatorPosition() { 1495 | return mSelectedPosition + mSelectionOffset; 1496 | } 1497 | 1498 | @Override 1499 | protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { 1500 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1501 | 1502 | if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) { 1503 | // HorizontalScrollView will first measure use with UNSPECIFIED, and then with 1504 | // EXACTLY. Ignore the first call since anything we do will be overwritten anyway 1505 | return; 1506 | } 1507 | 1508 | if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) { 1509 | final int count = getChildCount(); 1510 | 1511 | // First we'll find the widest tab 1512 | int largestTabWidth = 0; 1513 | for (int i = 0, z = count; i < z; i++) { 1514 | View child = getChildAt(i); 1515 | if (child.getVisibility() == VISIBLE) { 1516 | largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth()); 1517 | } 1518 | } 1519 | 1520 | if (largestTabWidth <= 0) { 1521 | // If we don't have a largest child yet, skip until the next measure pass 1522 | return; 1523 | } 1524 | 1525 | final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN); 1526 | boolean remeasure = false; 1527 | 1528 | if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) { 1529 | // If the tabs fit within our width minus gutters, we will set all tabs to have 1530 | // the same width 1531 | for (int i = 0; i < count; i++) { 1532 | final LinearLayout.LayoutParams lp = 1533 | (LayoutParams) getChildAt(i).getLayoutParams(); 1534 | if (lp.width != largestTabWidth || lp.weight != 0) { 1535 | lp.width = largestTabWidth; 1536 | lp.weight = 0; 1537 | remeasure = true; 1538 | } 1539 | } 1540 | } else { 1541 | // If the tabs will wrap to be larger than the width minus gutters, we need 1542 | // to switch to GRAVITY_FILL 1543 | mTabGravity = GRAVITY_FILL; 1544 | updateTabViews(false); 1545 | remeasure = true; 1546 | } 1547 | 1548 | if (remeasure) { 1549 | // Now re-measure after our changes 1550 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1551 | } 1552 | } 1553 | } 1554 | 1555 | @Override 1556 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 1557 | super.onLayout(changed, l, t, r, b); 1558 | 1559 | if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) { 1560 | // If we're currently running an animation, lets cancel it and start a 1561 | // new animation with the remaining duration 1562 | mCurrentAnimator.cancel(); 1563 | final long duration = mCurrentAnimator.getDuration(); 1564 | animateIndicatorToPosition(mSelectedPosition, 1565 | Math.round((1f - mCurrentAnimator.getAnimatedFraction()) * duration)); 1566 | } else { 1567 | // If we've been layed out, update the indicator position 1568 | updateIndicatorPosition(); 1569 | } 1570 | } 1571 | 1572 | private void updateIndicatorPosition() { 1573 | final View selectedTitle = getChildAt(mSelectedPosition); 1574 | int left, right; 1575 | 1576 | if (selectedTitle != null && selectedTitle.getWidth() > 0) { 1577 | left = selectedTitle.getLeft(); 1578 | right = selectedTitle.getRight(); 1579 | 1580 | if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) { 1581 | // Draw the selection partway between the tabs 1582 | View nextTitle = getChildAt(mSelectedPosition + 1); 1583 | left = (int) (mSelectionOffset * nextTitle.getLeft() + 1584 | (1.0f - mSelectionOffset) * left); 1585 | right = (int) (mSelectionOffset * nextTitle.getRight() + 1586 | (1.0f - mSelectionOffset) * right); 1587 | } 1588 | } else { 1589 | left = right = -1; 1590 | } 1591 | 1592 | setIndicatorPosition(left, right); 1593 | } 1594 | 1595 | private void setIndicatorPosition(int left, int right) { 1596 | if (left != mIndicatorLeft || right != mIndicatorRight) { 1597 | // If the indicator's left/right has changed, invalidate 1598 | mIndicatorLeft = left; 1599 | mIndicatorRight = right; 1600 | ViewCompat.postInvalidateOnAnimation(this); 1601 | } 1602 | } 1603 | 1604 | void animateIndicatorToPosition(final int position, int duration) { 1605 | final boolean isRtl = ViewCompat.getLayoutDirection(this) 1606 | == ViewCompat.LAYOUT_DIRECTION_RTL; 1607 | 1608 | final View targetView = getChildAt(position); 1609 | final int targetLeft = targetView.getLeft(); 1610 | final int targetRight = targetView.getRight(); 1611 | final int startLeft; 1612 | final int startRight; 1613 | 1614 | if (Math.abs(position - mSelectedPosition) <= 1) { 1615 | // If the views are adjacent, we'll animate from edge-to-edge 1616 | startLeft = mIndicatorLeft; 1617 | startRight = mIndicatorRight; 1618 | } else { 1619 | // Else, we'll just grow from the nearest edge 1620 | final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET); 1621 | if (position < mSelectedPosition) { 1622 | // We're going end-to-start 1623 | if (isRtl) { 1624 | startLeft = startRight = targetLeft - offset; 1625 | } else { 1626 | startLeft = startRight = targetRight + offset; 1627 | } 1628 | } else { 1629 | // We're going start-to-end 1630 | if (isRtl) { 1631 | startLeft = startRight = targetRight + offset; 1632 | } else { 1633 | startLeft = startRight = targetLeft - offset; 1634 | } 1635 | } 1636 | } 1637 | 1638 | if (startLeft != targetLeft || startRight != targetRight) { 1639 | ValueAnimator animator = mIndicatorAnimator = new ValueAnimator(); 1640 | animator.setInterpolator(new FastOutSlowInInterpolator()); 1641 | animator.setDuration(duration); 1642 | animator.setFloatValues(0, 1); 1643 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1644 | @Override 1645 | public void onAnimationUpdate(ValueAnimator animator) { 1646 | final float fraction = animator.getAnimatedFraction(); 1647 | setIndicatorPosition( 1648 | lerp(startLeft, targetLeft, fraction), 1649 | lerp(startRight, targetRight, fraction)); 1650 | } 1651 | }); 1652 | animator.addListener(new AnimatorListenerAdapter() { 1653 | 1654 | @Override 1655 | public void onAnimationEnd(Animator animator) { 1656 | mSelectedPosition = position; 1657 | mSelectionOffset = 0f; 1658 | } 1659 | 1660 | @Override 1661 | public void onAnimationCancel(Animator animator) { 1662 | mSelectedPosition = position; 1663 | mSelectionOffset = 0f; 1664 | } 1665 | }); 1666 | animator.start(); 1667 | mCurrentAnimator = animator; 1668 | } 1669 | } 1670 | 1671 | private int lerp(int startValue, int endValue, float fraction) { 1672 | return startValue + Math.round(fraction * (endValue - startValue)); 1673 | } 1674 | 1675 | @Override 1676 | public void draw(Canvas canvas) { 1677 | super.draw(canvas); 1678 | 1679 | // Thick colored underline below the current selection 1680 | if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) { 1681 | canvas.drawRect(mIndicatorLeft + getTabMargin(), getHeight() - mSelectedIndicatorHeight, 1682 | mIndicatorRight - getTabMargin(), getHeight(), mSelectedIndicatorPaint); 1683 | } 1684 | } 1685 | } 1686 | 1687 | private static ColorStateList createColorStateList(int defaultColor, int selectedColor) { 1688 | final int[][] states = new int[2][]; 1689 | final int[] colors = new int[2]; 1690 | int i = 0; 1691 | 1692 | states[i] = SELECTED_STATE_SET; 1693 | colors[i] = selectedColor; 1694 | i++; 1695 | 1696 | // Default enabled state 1697 | states[i] = EMPTY_STATE_SET; 1698 | colors[i] = defaultColor; 1699 | i++; 1700 | 1701 | return new ColorStateList(states, colors); 1702 | } 1703 | 1704 | private int getDefaultHeight() { 1705 | boolean hasIconAndText = false; 1706 | for (int i = 0, count = mTabs.size(); i < count; i++) { 1707 | Tab tab = mTabs.get(i); 1708 | if (tab != null && tab.getIcon() != null && !TextUtils.isEmpty(tab.getText())) { 1709 | hasIconAndText = true; 1710 | break; 1711 | } 1712 | } 1713 | return hasIconAndText ? DEFAULT_HEIGHT_WITH_TEXT_ICON : DEFAULT_HEIGHT; 1714 | } 1715 | 1716 | private int getTabMinWidth() { 1717 | if (mRequestedTabMinWidth != INVALID_WIDTH) { 1718 | // If we have been given a min width, use it 1719 | return mRequestedTabMinWidth; 1720 | } 1721 | // Else, we'll use the default value 1722 | return mMode == MODE_SCROLLABLE ? mScrollableTabMinWidth : 0; 1723 | } 1724 | 1725 | private int getTabMaxWidth() { 1726 | return mTabMaxWidth; 1727 | } 1728 | 1729 | /** 1730 | * A {@link ViewPager.OnPageChangeListener} class which contains the 1731 | * necessary calls back to the provided {@link MyTabLayout} so that the tab position is 1732 | * kept in sync. 1733 | *

1734 | *

This class stores the provided TabLayout weakly, meaning that you can use 1735 | * {@link ViewPager#addOnPageChangeListener(ViewPager.OnPageChangeListener) 1736 | * addOnPageChangeListener(OnPageChangeListener)} without removing the listener and 1737 | * not cause a leak. 1738 | */ 1739 | public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener { 1740 | private final WeakReference mTabLayoutRef; 1741 | private int mPreviousScrollState; 1742 | private int mScrollState; 1743 | 1744 | public TabLayoutOnPageChangeListener(MyTabLayout tabLayout) { 1745 | mTabLayoutRef = new WeakReference<>(tabLayout); 1746 | } 1747 | 1748 | @Override 1749 | public void onPageScrollStateChanged(int state) { 1750 | mPreviousScrollState = mScrollState; 1751 | mScrollState = state; 1752 | } 1753 | 1754 | @Override 1755 | public void onPageScrolled(int position, float positionOffset, 1756 | int positionOffsetPixels) { 1757 | final MyTabLayout tabLayout = mTabLayoutRef.get(); 1758 | if (tabLayout != null) { 1759 | // Update the scroll position, only update the text selection if we're being 1760 | // dragged (or we're settling after a drag) 1761 | final boolean updateText = (mScrollState == ViewPager.SCROLL_STATE_DRAGGING) 1762 | || (mScrollState == ViewPager.SCROLL_STATE_SETTLING 1763 | && mPreviousScrollState == ViewPager.SCROLL_STATE_DRAGGING); 1764 | tabLayout.setScrollPosition(position, positionOffset, updateText); 1765 | } 1766 | } 1767 | 1768 | @Override 1769 | public void onPageSelected(int position) { 1770 | final MyTabLayout tabLayout = mTabLayoutRef.get(); 1771 | if (tabLayout != null && tabLayout.getSelectedTabPosition() != position) { 1772 | // Select the tab, only updating the indicator if we're not being dragged/settled 1773 | // (since onPageScrolled will handle that). 1774 | tabLayout.selectTab(tabLayout.getTabAt(position), 1775 | mScrollState == ViewPager.SCROLL_STATE_IDLE); 1776 | } 1777 | } 1778 | } 1779 | 1780 | /** 1781 | * A {@link MyTabLayout.OnTabSelectedListener} class which contains the necessary calls back 1782 | * to the provided {@link ViewPager} so that the tab position is kept in sync. 1783 | */ 1784 | public static class ViewPagerOnTabSelectedListener implements MyTabLayout.OnTabSelectedListener { 1785 | private final ViewPager mViewPager; 1786 | 1787 | public ViewPagerOnTabSelectedListener(ViewPager viewPager) { 1788 | mViewPager = viewPager; 1789 | } 1790 | 1791 | @Override 1792 | public void onTabSelected(MyTabLayout.Tab tab) { 1793 | mViewPager.setCurrentItem(tab.getPosition()); 1794 | } 1795 | 1796 | @Override 1797 | public void onTabUnselected(MyTabLayout.Tab tab) { 1798 | // No-op 1799 | } 1800 | 1801 | @Override 1802 | public void onTabReselected(MyTabLayout.Tab tab) { 1803 | // No-op 1804 | } 1805 | } 1806 | 1807 | //******************************以下都是我为了以后的方便添加的***********************************************************// 1808 | 1809 | private int mTabMargin; 1810 | private int mLastCheckedPos = -1; 1811 | private Paint mPaint; 1812 | private int mBottomLineColor = Color.parseColor("#dcdcdc"); 1813 | 1814 | public MyTabLayout addTabs(String... tabs) { 1815 | if (tabs == null || tabs.length == 0) 1816 | return this; 1817 | 1818 | for (int i = 0; i < tabs.length; i++) { 1819 | this.addTab(this.newTab().setText(tabs[i]).setTag(i)); 1820 | } 1821 | return this; 1822 | } 1823 | 1824 | public void setSelectTab(int tabPos) { 1825 | getTabAt(tabPos).select(); 1826 | } 1827 | 1828 | public MyTabLayout setTabMargin(int margin) { 1829 | mTabMargin = dpToPx(margin); 1830 | return this; 1831 | } 1832 | 1833 | public int getTabMargin() { 1834 | return mTabMargin; 1835 | } 1836 | 1837 | public MyTabLayout setBottomLineColor(int lineColor) { 1838 | this.mBottomLineColor = lineColor; 1839 | return this; 1840 | } 1841 | 1842 | 1843 | public interface MaterialTabSelectedListener { 1844 | void onTabSelected(Tab tab, boolean reSelected); 1845 | } 1846 | 1847 | public MyTabLayout setOnMaterialTabSelectedListener(MaterialTabSelectedListener listener1) { 1848 | this.listener = listener1; 1849 | setOnTabSelectedListener(new MaterialTabSelectedAdapter()); 1850 | // setSelectTab(0); 1851 | return this; 1852 | } 1853 | 1854 | private MaterialTabSelectedListener listener; 1855 | 1856 | public class MaterialTabSelectedAdapter implements OnTabSelectedListener { 1857 | 1858 | @Override 1859 | public void onTabSelected(Tab tab) { 1860 | if (listener != null) { 1861 | listener.onTabSelected(tab, false); 1862 | mLastCheckedPos = tab.getPosition(); 1863 | } 1864 | } 1865 | 1866 | @Override 1867 | public void onTabUnselected(Tab tab) { 1868 | 1869 | } 1870 | 1871 | @Override 1872 | public void onTabReselected(Tab tab) { 1873 | if (listener != null) { 1874 | if (mLastCheckedPos == -1) { 1875 | onTabSelected(tab); 1876 | } else { 1877 | listener.onTabSelected(tab, true); 1878 | } 1879 | } 1880 | } 1881 | } 1882 | 1883 | @Override 1884 | protected void onDraw(Canvas canvas) { 1885 | super.onDraw(canvas); 1886 | 1887 | initPaintIfNeeded(); 1888 | canvas.drawLine(0, 1889 | getHeight(), getWidth(), getHeight(), mPaint); 1890 | } 1891 | 1892 | private void initPaintIfNeeded() { 1893 | if (mPaint == null) { 1894 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 1895 | mPaint.setStyle(Paint.Style.FILL_AND_STROKE); 1896 | mPaint.setStrokeWidth(2); 1897 | mPaint.setColor(mBottomLineColor); 1898 | } 1899 | } 1900 | } 1901 | -------------------------------------------------------------------------------- /app/src/main/java/com/qiaomu/tablerow/TableRowTextView.java: -------------------------------------------------------------------------------- 1 | package com.qiaomu.tablerow; 2 | 3 | /** 4 | * Created by qiaomu on 2017/9/4. 5 | */ 6 | 7 | 8 | import android.content.Context; 9 | import android.content.res.TypedArray; 10 | import android.graphics.Bitmap; 11 | import android.graphics.Canvas; 12 | import android.graphics.Color; 13 | import android.graphics.Paint; 14 | import android.graphics.Rect; 15 | import android.graphics.RectF; 16 | import android.graphics.drawable.Drawable; 17 | import android.os.Build; 18 | import android.support.annotation.RequiresApi; 19 | import android.support.v4.text.TextDirectionHeuristicsCompat; 20 | import android.support.v7.widget.AppCompatTextView; 21 | import android.text.Layout; 22 | import android.text.Spannable; 23 | import android.text.SpannableStringBuilder; 24 | import android.text.StaticLayout; 25 | import android.text.TextDirectionHeuristics; 26 | import android.text.TextPaint; 27 | import android.text.TextUtils; 28 | import android.util.AttributeSet; 29 | import android.util.Log; 30 | import android.widget.ImageView; 31 | 32 | import java.lang.reflect.Constructor; 33 | import java.lang.reflect.InvocationTargetException; 34 | import java.util.ArrayList; 35 | import java.util.List; 36 | 37 | /** 38 | * Created by mrs on 2016/9/13. 39 | */ 40 | public class TableRowTextView extends AppCompatTextView { 41 | private static final String TAG = "TableRowTextView"; 42 | private static final int WRAP_CONTENT = 1; 43 | private static final int FIX_WIDTH = 0; 44 | private static final int ALIGN_NORMAL = 0; 45 | private static final int ALIGN_CENTER = 1; 46 | private static final String ELLIPSIZE = "..."; 47 | private float mEllipsizeWidth; 48 | 49 | protected boolean mShouldDrawBotLine = false;//是否绘制表单底部线条 50 | protected boolean mShouldDrawTopLine = false;//是否绘制表单顶部线条 51 | protected boolean mShouldDrawLeftLine = false;//是否绘制表单最坐侧分割线 52 | protected boolean mShouldDrawRightLine = false;//是否绘制表单最右侧分割线 53 | 54 | private boolean mCellDivider;//是否回绘制单元格分割线 55 | protected int mFixCellWidth = dp2px(45);//单元格宽度,默认45dp,如果不够充满屏幕,单元格宽度会被增大 56 | private int mCellMode; 57 | private int mDividerColor = Color.parseColor("#f4f4f4"); 58 | private float mCellDividerPadding; 59 | private int mRowDivider = 0x00; 60 | private int mCellAlign;//文本对齐方式 61 | private Layout.Alignment mAlignment; 62 | private StaticLayout mLayout; 63 | private int mMaxlines; 64 | protected List mList; 65 | protected TextPaint mLinePaint, mTxtPaint; 66 | 67 | public TableRowTextView(Context context) { 68 | this(context, null); 69 | } 70 | 71 | public TableRowTextView(Context context, AttributeSet attrs) { 72 | this(context, attrs, 0); 73 | } 74 | 75 | public TableRowTextView(Context context, AttributeSet attrs, int defStyleAttr) { 76 | super(context, attrs, defStyleAttr); 77 | 78 | TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.TableRowTextView); 79 | 80 | mCellDivider = ta.getBoolean(R.styleable.TableRowTextView_cell_divider, false); 81 | 82 | mFixCellWidth = ta.getDimensionPixelSize(R.styleable.TableRowTextView_fixWidth, mFixCellWidth); 83 | 84 | mCellMode = ta.getInt(R.styleable.TableRowTextView_cell_mode, FIX_WIDTH); 85 | mCellAlign = ta.getInt(R.styleable.TableRowTextView_align, 0); 86 | mAlignment = mCellAlign == ALIGN_NORMAL ? Layout.Alignment.ALIGN_NORMAL : Layout.Alignment.ALIGN_CENTER; 87 | 88 | mRowDivider = ta.getInt(R.styleable.TableRowTextView_row_divider, 0x00); 89 | mShouldDrawLeftLine = ((mRowDivider & 0x01) == 0x01); 90 | mShouldDrawTopLine = ((mRowDivider & 0x10) == 0x10); 91 | mShouldDrawRightLine = ((mRowDivider & 0x02) == 0x02); 92 | mShouldDrawBotLine = ((mRowDivider & 0x20) == 0x20); 93 | 94 | ta.recycle(); 95 | 96 | init(); 97 | } 98 | 99 | @Override 100 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 101 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 102 | if (mList == null || mList.size() <= 0) 103 | return; 104 | 105 | int totalWidth = 0; 106 | if (mCellMode == FIX_WIDTH) { 107 | totalWidth = mList.size() * mFixCellWidth; 108 | totalWidth = Math.max(totalWidth, getMeasuredWidth()); 109 | mFixCellWidth = totalWidth / mList.size(); 110 | } else if (mCellMode == WRAP_CONTENT) { 111 | float wrapLength = 0; 112 | int wrapCount = 0; 113 | for (int i = 0; i < mList.size(); i++) { 114 | CharSequence charSequence = mList.get(i); 115 | float measureText = getTextWidth(charSequence); 116 | if (measureText > mFixCellWidth) { 117 | wrapLength += measureText; 118 | totalWidth += measureText; 119 | wrapCount++; 120 | } else { 121 | totalWidth += mFixCellWidth; 122 | } 123 | 124 | } 125 | //如果内容区域一共是 500,控件宽1080 那么强制占满屏 126 | if (totalWidth <= getMeasuredWidth()) { 127 | totalWidth = getMeasuredWidth(); 128 | mFixCellWidth = (int) ((totalWidth - wrapLength) / (mList.size() - wrapCount)); 129 | } 130 | Log.e(TAG, "onMeasure: " + wrapLength + "--" + wrapCount + "--" + (totalWidth - wrapLength)); 131 | } 132 | 133 | int measureSpec = MeasureSpec.makeMeasureSpec(totalWidth, MeasureSpec.EXACTLY); 134 | setMeasuredDimension(measureSpec, heightMeasureSpec); 135 | 136 | } 137 | 138 | 139 | protected void init() { 140 | if (mLinePaint == null) { 141 | mLinePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); 142 | mLinePaint.setColor(getCurrentTextColor()); 143 | mLinePaint.setStrokeWidth(1); 144 | 145 | mTxtPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); 146 | mTxtPaint.setTextSize(getTextSize()); 147 | mTxtPaint.setColor(getCurrentTextColor()); 148 | mTxtPaint.setTextSize(getTextSize()); 149 | 150 | mEllipsizeWidth = mTxtPaint.measureText(ELLIPSIZE); 151 | } 152 | } 153 | 154 | @Override 155 | protected void onDraw(Canvas canvas) { 156 | super.onDraw(canvas); 157 | if (mList == null || mList.size() <= 0) 158 | return; 159 | drawFixWidthCharSequence(canvas); 160 | 161 | drawWrapContentCharSequence(canvas); 162 | 163 | drawDividers(canvas); 164 | 165 | } 166 | 167 | //绘制单元格宽度自适应下的文字分布 168 | private void drawWrapContentCharSequence(Canvas canvas) { 169 | if (mCellMode != WRAP_CONTENT) 170 | return; 171 | float previousRight = 0f; 172 | for (int i = 0; i < mList.size(); i++) { 173 | int height = getMeasuredHeight(); 174 | CharSequence txt = mList.get(i) == null ? "" : mList.get(i); 175 | int textWidth = getTextWidth(txt); 176 | int outerWidth = textWidth <= mFixCellWidth ? mFixCellWidth : textWidth; 177 | //StaticLayout不知道是啥的自行百度 178 | mLayout = new StaticLayout(txt, 0, txt.length(), 179 | mTxtPaint, outerWidth, mAlignment, 1.0f, 0f, false); 180 | 181 | float startX = 0f; 182 | if (textWidth < mFixCellWidth) { 183 | if (mCellAlign == ALIGN_NORMAL) { 184 | startX = previousRight + mFixCellWidth / 2 - getMaxLineWidth() / 2; 185 | previousRight += mFixCellWidth; 186 | } else { 187 | startX = previousRight; 188 | previousRight += mFixCellWidth; 189 | } 190 | } else { 191 | if (mCellAlign == ALIGN_NORMAL) { 192 | startX = previousRight + (mLayout.getWidth() - mLayout.getLineWidth(0)) / 2;//previousRight; 193 | previousRight += textWidth; 194 | } else { 195 | startX = previousRight; 196 | previousRight += textWidth; 197 | } 198 | } 199 | 200 | float baseline = getHeight() / 2 - mLayout.getHeight() / 2; 201 | canvas.save(); 202 | canvas.translate(startX, baseline); 203 | mLayout.draw(canvas); 204 | canvas.restore(); 205 | drawCellDivider(canvas, previousRight); 206 | } 207 | } 208 | 209 | //绘制单元格宽度固定下的文字分布 210 | private void drawFixWidthCharSequence(Canvas canvas) { 211 | if (mCellMode != FIX_WIDTH) 212 | return; 213 | 214 | for (int i = 0; i < mList.size(); i++) { 215 | CharSequence txt = getFixCharSequence(mList.get(i)); 216 | //StaticLayout不知道是啥的自行百度 217 | mLayout = new StaticLayout(txt, 0, txt.length(), mTxtPaint, 218 | mFixCellWidth, mAlignment, 1.0f, 0f, true); 219 | float x = 0; 220 | if (mCellAlign == ALIGN_NORMAL) { 221 | x = mFixCellWidth * (i + 1) - mFixCellWidth / 2 - getMaxLineWidth() / 2; 222 | } else { 223 | x = mFixCellWidth * i; 224 | } 225 | float baseline = getHeight() / 2 - mLayout.getHeight() / 2; 226 | canvas.save(); 227 | canvas.translate(x, baseline); 228 | mLayout.draw(canvas); 229 | canvas.restore(); 230 | drawCellDivider(canvas, mFixCellWidth * (i + 1)); 231 | } 232 | } 233 | 234 | //绘制单元格分割线 235 | private void drawCellDivider(Canvas canvas, float startx) { 236 | if (mCellDivider) { 237 | canvas.drawLine(startx, 0, startx, getHeight(), mLinePaint);//右边的线 238 | } 239 | } 240 | 241 | //绘制左上右下的分割线 242 | private void drawDividers(Canvas canvas) { 243 | if (mShouldDrawLeftLine) 244 | canvas.drawLine(0, canvas.getHeight(), 0, canvas.getHeight(), mLinePaint); 245 | 246 | if (mShouldDrawTopLine) 247 | canvas.drawLine(0, 0, canvas.getWidth(), 0, mLinePaint); 248 | 249 | 250 | if (mShouldDrawRightLine) 251 | canvas.drawLine(canvas.getWidth(), 0, canvas.getWidth(), canvas.getHeight(), mLinePaint); 252 | 253 | 254 | if (mShouldDrawLeftLine) 255 | canvas.drawLine(0, canvas.getHeight(), canvas.getWidth(), canvas.getHeight(), mLinePaint); 256 | } 257 | 258 | 259 | public void setDrawTableDividers(boolean left, boolean top, boolean right, boolean bottom, boolean cellDivider, float cellDividerPadding) { 260 | mShouldDrawLeftLine = left; 261 | mShouldDrawTopLine = top; 262 | mShouldDrawRightLine = right; 263 | mShouldDrawBotLine = bottom; 264 | mCellDivider = cellDivider; 265 | mCellDividerPadding = cellDividerPadding; 266 | } 267 | 268 | public void setTextList(List lists) { 269 | if (lists == null) 270 | return; 271 | this.mList = lists; 272 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { 273 | if (!isInLayout()) 274 | requestLayout(); 275 | } 276 | invalidate(); 277 | } 278 | 279 | public void setTextArray(CharSequence... textArray) { 280 | if (textArray != null) { 281 | ArrayList list = new ArrayList(); 282 | for (int i = 0; i < textArray.length; i++) { 283 | list.add(textArray[i]); 284 | } 285 | setTextList(list); 286 | } 287 | } 288 | 289 | @Override 290 | public void setMaxLines(int maxlines) { 291 | mMaxlines = maxlines; 292 | super.setMaxLines(maxlines); 293 | } 294 | 295 | //计算需要截断的文本 296 | private CharSequence getFixCharSequence(CharSequence txt) { 297 | if (getFixWidthLines(txt) <= mMaxlines || mMaxlines == 0) 298 | return txt; 299 | float totalWidth = mEllipsizeWidth; 300 | int overIndex = 0; 301 | 302 | float[] widths = new float[txt.length()]; 303 | mTxtPaint.getTextWidths(txt, 0, txt.length(), widths); 304 | for (int j = 0; j < txt.length(); j++) { 305 | totalWidth += (int) Math.ceil(widths[j]); 306 | if (totalWidth >= mFixCellWidth * mMaxlines) { 307 | overIndex = j; 308 | break; 309 | } 310 | } 311 | if (txt instanceof SpannableStringBuilder) { 312 | SpannableStringBuilder replace = ((SpannableStringBuilder) txt).delete(overIndex - 1, txt.length()); 313 | return replace.append(ELLIPSIZE); 314 | } 315 | 316 | return txt.subSequence(0, overIndex) + ELLIPSIZE; 317 | } 318 | 319 | //精确地得到文字宽度 320 | private int getTextWidth(CharSequence str) { 321 | int w = 0; 322 | if (str != null && str.length() > 0) { 323 | int len = str.length(); 324 | float[] widths = new float[len]; 325 | mTxtPaint.getTextWidths(str, 0, str.length(), widths); 326 | for (int j = 0; j < len; j++) { 327 | w += (int) Math.ceil(widths[j]); 328 | } 329 | } 330 | return w; 331 | } 332 | 333 | //计算文本行数 334 | private int getFixWidthLines(CharSequence txt) { 335 | float measureText = mTxtPaint.measureText(txt, 0, txt.length()); 336 | int lineCount = 1; 337 | if (measureText > mFixCellWidth) { 338 | lineCount = (int) (measureText / mFixCellWidth) + 1; 339 | } 340 | return lineCount; 341 | } 342 | 343 | //得到 344 | private RectF getCharSequenceRect(CharSequence sequence) { 345 | if (sequence instanceof Spannable) { 346 | mLayout = new StaticLayout(sequence, mTxtPaint, 10000, Layout.Alignment.ALIGN_CENTER, 1.0f, 0f, false); 347 | float textHeight = mLayout.getHeight(); 348 | float maxLineWidth = getMaxLineWidth(); 349 | return new RectF(0, 0, maxLineWidth, textHeight); 350 | } else { 351 | Paint.FontMetrics fontMetrics = mTxtPaint.getFontMetrics(); 352 | float textHeight = fontMetrics.descent - fontMetrics.ascent; 353 | float textWidth = mTxtPaint.measureText(sequence, 0, getCharSequenceLength(sequence)); 354 | return new RectF(0, 0, textWidth, textHeight); 355 | } 356 | 357 | } 358 | 359 | //获取所有行中字符最长的哪一行的宽度 360 | private float getMaxLineWidth() { 361 | int lineCount = mLayout.getLineCount(); 362 | float maxWidth = mLayout.getLineWidth(0); 363 | 364 | for (int i = 0; i < lineCount; i++) { 365 | Math.max(maxWidth, mLayout.getLineWidth(i)); 366 | } 367 | return maxWidth; 368 | 369 | } 370 | 371 | private int getCharSequenceLength(CharSequence source) { 372 | return TextUtils.isEmpty(source) ? 0 : source.length(); 373 | } 374 | 375 | private int dp2px(float dpValue) { 376 | float density = getResources().getDisplayMetrics().density; 377 | return (int) (dpValue * density + 0.5f); 378 | } 379 | 380 | private int sp2px(float spValue) { 381 | float density = getResources().getDisplayMetrics().scaledDensity; 382 | return (int) (spValue * density + 0.5f); 383 | } 384 | } 385 | 386 | -------------------------------------------------------------------------------- /app/src/main/java/com/qiaomu/tablerow/chart/Axis.java: -------------------------------------------------------------------------------- 1 | package com.qiaomu.tablerow.chart; 2 | 3 | /** 4 | * Created by qiaomu on 2017/7/20. 5 | * 坐标轴的基础信息 6 | */ 7 | public class Axis { 8 | private int DEFAULT_COLOR = ChartConf.DEFAULT_COLOR; 9 | //是否显示分割线 10 | private boolean mDisplayDivline = false; 11 | //是否显示箭头 12 | private boolean mDisplayArrow = false; 13 | //坐标轴颜色 14 | private int mAxisColor = DEFAULT_COLOR; 15 | //最小值 16 | private int mMinValue = 0; 17 | //刻度步进值 18 | private int mStep; 19 | //最大值 20 | private int mMaxValue = Integer.MAX_VALUE; 21 | //坐标轴刻度值颜色 22 | private int mCoordinateColor = DEFAULT_COLOR; 23 | //坐标轴刻度值大小 24 | private int mCoordinateSize = ChartConf.DEFAULT_SUB_SIZE;//sp 25 | //是否显示坐标轴刻度值 26 | private boolean mDisplayCoordinateValue = true; 27 | //坐标轴刻度值旋转角度 28 | private int mRotateDegrees = 0; 29 | 30 | private float mCoordinateThick=ChartConf.DEFAULT_STROKE_WIDTH; 31 | 32 | private CharSequence mStartValue; 33 | private CharSequence mEndValue; 34 | 35 | 36 | public boolean isDisplayDivider() { 37 | return mDisplayDivline; 38 | } 39 | 40 | public void setDisplayDivider(boolean displayDivider) { 41 | mDisplayDivline = displayDivider; 42 | } 43 | 44 | public boolean isDisplayArrow() { 45 | return mDisplayArrow; 46 | } 47 | 48 | public void setDisplayArrow(boolean displayArrow) { 49 | mDisplayArrow = displayArrow; 50 | } 51 | 52 | public int getAxisColor() { 53 | return mAxisColor; 54 | } 55 | 56 | public void setAxisColor(int axisColor) { 57 | mAxisColor = axisColor; 58 | } 59 | 60 | public int getMinValue() { 61 | return mMinValue; 62 | } 63 | 64 | public void setMinValue(int minValue) { 65 | mMinValue = minValue; 66 | } 67 | 68 | public int getMaxValue() { 69 | return mMaxValue; 70 | } 71 | 72 | public void setMaxValue(int maxValue) { 73 | mMaxValue = maxValue; 74 | } 75 | 76 | public int getCoordinateColor() { 77 | return mCoordinateColor; 78 | } 79 | 80 | public void setCoordinateColor(int coordinateColor) { 81 | mCoordinateColor = coordinateColor; 82 | } 83 | 84 | public int getCoordinateSize() { 85 | return mCoordinateSize; 86 | } 87 | 88 | public void setCoordinateSize(int coordinateSize) { 89 | mCoordinateSize = coordinateSize; 90 | } 91 | 92 | public boolean isDisplayCoordinateValue() { 93 | return mDisplayCoordinateValue; 94 | } 95 | 96 | public void setDisplayCoordinateValue(boolean displayCoordinateValue) { 97 | mDisplayCoordinateValue = displayCoordinateValue; 98 | } 99 | 100 | public int getRotateDegrees() { 101 | return mRotateDegrees; 102 | } 103 | 104 | public void setRotateDegrees(int rotateDegrees) { 105 | mRotateDegrees = rotateDegrees; 106 | } 107 | 108 | public int getStep() { 109 | return mStep; 110 | } 111 | 112 | public void setStep(int step) { 113 | mStep = step; 114 | } 115 | 116 | public void setStartValue(CharSequence startValue) { 117 | mStartValue = startValue; 118 | } 119 | 120 | public void setEndValue(CharSequence endValue) { 121 | mEndValue = endValue; 122 | } 123 | public float getCoordinateThick() { 124 | return mCoordinateThick; 125 | } 126 | 127 | public void setCoordinateThick(float coordinateThick) { 128 | mCoordinateThick = coordinateThick; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /app/src/main/java/com/qiaomu/tablerow/chart/BaseEntry.java: -------------------------------------------------------------------------------- 1 | package com.qiaomu.tablerow.chart; 2 | 3 | import android.support.annotation.ColorInt; 4 | 5 | /** 6 | * Created by qiaomu on 2017/8/2. 7 | */ 8 | 9 | public class BaseEntry { 10 | /** 11 | * 扇形描述性文字,你可以设置spannableString等实现类对象 12 | */ 13 | private CharSequence mCharSequence; 14 | /** 15 | * 每个扇形的颜色 16 | */ 17 | private int mChartColor = ChartConf.DEFAULT_COLOR; 18 | 19 | 20 | public CharSequence getCharSequence() { 21 | return mCharSequence; 22 | } 23 | 24 | public void setCharSequence(CharSequence charSequence) { 25 | mCharSequence = charSequence; 26 | } 27 | 28 | 29 | public int getChartColor() { 30 | return mChartColor; 31 | } 32 | 33 | public void setChartColor(@ColorInt int chartColor) { 34 | mChartColor = chartColor; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/qiaomu/tablerow/chart/ChartConf.java: -------------------------------------------------------------------------------- 1 | package com.qiaomu.tablerow.chart; 2 | 3 | import android.graphics.Color; 4 | 5 | /** 6 | * Created by qiaomu on 2017/7/21. 7 | */ 8 | 9 | public class ChartConf { 10 | public static final int DEFAULT_COLOR = Color.GRAY; 11 | public static final int DEFAULT_SIZE = 28; 12 | public static final int DEFAULT_SUB_SIZE = 24; 13 | public static final int DEFAULT_STROKE_WIDTH = 2; 14 | public static final int DEFAULT_LENGTH10dp = 20; 15 | public static final float DEFAULT_SPACINGMULT = 1.0f; 16 | public static final float DEFAULT_SPACINGADD = 0f; 17 | public static final int DEFAULT_PADDING = 5; 18 | public static final int DEFAULT_RECT_WIDTH = 15; 19 | public static final int DEFAULT_BAR_WIDTH = 20; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/qiaomu/tablerow/chart/ChartView.java: -------------------------------------------------------------------------------- 1 | package com.qiaomu.tablerow.chart; 2 | 3 | import android.animation.ValueAnimator; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Paint; 9 | import android.graphics.RectF; 10 | import android.graphics.drawable.ColorDrawable; 11 | import android.graphics.drawable.Drawable; 12 | import android.support.annotation.Nullable; 13 | import android.text.Layout; 14 | import android.text.Spannable; 15 | import android.text.StaticLayout; 16 | import android.text.TextPaint; 17 | import android.text.TextUtils; 18 | import android.util.AttributeSet; 19 | import android.view.View; 20 | import android.view.animation.DecelerateInterpolator; 21 | 22 | import java.math.BigDecimal; 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | 26 | /** 27 | * Created by qiaomu on 2017/7/20. 28 | */ 29 | 30 | public abstract class ChartView extends View { 31 | public final String TAG = getClass().toString(); 32 | private Paint mAxisPaint; //坐标轴画笔 33 | private Paint mLegendPaint;//图例画笔 34 | private Paint mGraphPaint;//图形画笔 35 | private TextPaint mNotesPaint;//注释画笔 36 | private Paint mDividerPaint;//分割线 37 | private Paint mTitlePaint;//标题 38 | private Paint clearPaint; 39 | 40 | protected Axis mXaxis; 41 | protected Axis mYaxis; 42 | 43 | private Legend mLegend;//图例 44 | private CharSequence mTitle;//标题 45 | 46 | protected Bitmap mCacheBitmap; 47 | private Canvas mCacheCanvas;//双缓冲 48 | 49 | protected List mDatas; 50 | private ValueAnimator mValueAnimator; 51 | public RectF mRectF;//图形区域 52 | private long mAnimationDuration = 3000; 53 | protected float mExtraLeftPad, mExtraTopPad, mExtraRightPad, mExtraBottomPad; 54 | protected float mMaxPadding;//四周应该间隔的距离 55 | protected StaticLayout mLayout; 56 | private CharSequence mDescription = "暂无数据";//空状态下文字描述 57 | 58 | public ChartView(Context context) { 59 | this(context, null); 60 | } 61 | 62 | public ChartView(Context context, @Nullable AttributeSet attrs) { 63 | this(context, attrs, 0); 64 | } 65 | 66 | public ChartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 67 | super(context, attrs, defStyleAttr); 68 | setLayerType(View.LAYER_TYPE_SOFTWARE, null); 69 | } 70 | 71 | @Override 72 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 73 | super.onSizeChanged(w, h, oldw, oldh); 74 | if (mDatas == null || mDatas.size() == 0) 75 | return; 76 | 77 | calculateOffset(); 78 | 79 | makeChartRegion(); 80 | } 81 | 82 | /** 83 | * 在这个方法里设定 左{@link ChartView#getExtraPaddingLeft()} 84 | * --------------上{@link ChartView#getExtraPaddingTop()} 85 | * --------------右{@link ChartView#getExtraPaddingRight()} 86 | * --------------下 {@link ChartView#getExtraPaddingBottom()} 87 | * ---------------图形所在矩形应该偏移多少px 88 | */ 89 | 90 | 91 | @Override 92 | protected void onDraw(Canvas canvas) { 93 | super.onDraw(canvas); 94 | 95 | if (mDatas == null || mDatas.size() == 0) { 96 | drawDescription(canvas); 97 | return; 98 | } 99 | render(canvas); 100 | } 101 | 102 | /*mRectF = new RectF(getRectLeft(), 103 | getRectTop(), 104 | getRectWidth() + getRectLeft(), 105 | getRectHeight() + getRectTop() 106 | );*/ 107 | public abstract void calculateOffset(); 108 | 109 | public abstract void makeChartRegion(); 110 | 111 | public abstract void render(Canvas canvas); 112 | 113 | public Paint getAxisPaint() { 114 | if (mAxisPaint == null) { 115 | mAxisPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 116 | mAxisPaint.setAntiAlias(true); 117 | mAxisPaint.setDither(true); 118 | } 119 | return mAxisPaint; 120 | } 121 | 122 | public Paint getLegendPaint() { 123 | if (mLegendPaint == null) { 124 | mLegendPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 125 | mLegendPaint.setAntiAlias(true); 126 | mLegendPaint.setDither(true); 127 | } 128 | return mLegendPaint; 129 | } 130 | 131 | public Paint getGraphPaint() { 132 | if (mGraphPaint == null) { 133 | mGraphPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 134 | mGraphPaint.setAntiAlias(true); 135 | mGraphPaint.setDither(true); 136 | } 137 | return mGraphPaint; 138 | } 139 | 140 | public Paint getDividerPaint() { 141 | if (mDividerPaint == null) { 142 | mDividerPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 143 | mDividerPaint.setAntiAlias(true); 144 | mDividerPaint.setDither(true); 145 | } 146 | return mDividerPaint; 147 | } 148 | 149 | public TextPaint getNotesPaint() { 150 | if (mNotesPaint == null) { 151 | mNotesPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); 152 | mNotesPaint.setTextSize(ChartConf.DEFAULT_SIZE); 153 | } 154 | return mNotesPaint; 155 | } 156 | 157 | public Paint getTitlePaint() { 158 | if (mTitlePaint == null) { 159 | mTitlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 160 | mTitlePaint.setTextSize(ChartConf.DEFAULT_SIZE); 161 | } 162 | return mTitlePaint; 163 | } 164 | 165 | public Axis getXaxis() { 166 | if (mXaxis == null) mXaxis = new Axis(); 167 | return mXaxis; 168 | } 169 | 170 | public Axis getYaxis() { 171 | if (mYaxis == null) mYaxis = new Axis(); 172 | return mYaxis; 173 | } 174 | 175 | public Legend getLegend() { 176 | if (mLegend == null) 177 | mLegend = new Legend(); 178 | return mLegend; 179 | } 180 | 181 | public void setLegend(Legend legend) { 182 | mLegend = legend; 183 | } 184 | 185 | 186 | public void setTitle(CharSequence title) { 187 | mTitle = title; 188 | } 189 | 190 | public CharSequence getTitle() { 191 | return mTitle; 192 | } 193 | 194 | public int sp2px(float spValue) { 195 | final float fontScale = getResources().getDisplayMetrics().scaledDensity; 196 | return (int) (spValue * fontScale + 0.5f); 197 | } 198 | 199 | public int dp2px(float spValue) { 200 | final float fontScale = getResources().getDisplayMetrics().density; 201 | return (int) (spValue * fontScale + 0.5f); 202 | } 203 | 204 | 205 | public float getRectWidth() { 206 | return getWidth() - getExtraPaddingLeft() - 207 | getPaddingLeft() - getExtraPaddingRight() - getPaddingRight(); 208 | } 209 | 210 | public float getRectHeight() { 211 | return getHeight() - getExtraPaddingTop() - getPaddingTop() - getPaddingBottom(); 212 | } 213 | 214 | public float getRectLeft() { 215 | return getExtraPaddingLeft() + getPaddingLeft(); 216 | } 217 | 218 | public float getRectTop() { 219 | return getExtraPaddingTop() + getPaddingTop(); 220 | } 221 | 222 | public float getExtraPaddingLeft() { 223 | return mExtraLeftPad == 0 ? mMaxPadding : mExtraLeftPad; 224 | } 225 | 226 | public float getExtraPaddingTop() { 227 | return mExtraTopPad; //== 0 ? mMaxPadding : mExtraTopPad; 228 | } 229 | 230 | public float getExtraPaddingRight() { 231 | return mExtraRightPad == 0 ? getExtraPaddingLeft() : mExtraRightPad; 232 | } 233 | 234 | public float getExtraPaddingBottom() { 235 | return mExtraBottomPad;//== 0 ? getExtraPaddingTop() : mExtraBottomPad; 236 | } 237 | 238 | public int getChildCount() { 239 | return mDatas == null ? 0 : mDatas.size(); 240 | } 241 | 242 | 243 | public void clearAnimation() { 244 | if (mValueAnimator != null && mValueAnimator.isRunning()) 245 | mValueAnimator.cancel(); 246 | } 247 | 248 | public ValueAnimator getValueAnimator() { 249 | if (mValueAnimator == null) { 250 | mValueAnimator = ValueAnimator.ofFloat(0, 1); 251 | mValueAnimator.setInterpolator(new DecelerateInterpolator(3f)); 252 | mValueAnimator.setDuration(mAnimationDuration); 253 | } 254 | return mValueAnimator; 255 | } 256 | 257 | float lastAnimatedFraction = 0F; 258 | 259 | public void startValueAnimation(final ValueAnimator.AnimatorUpdateListener updateListener) { 260 | lastAnimatedFraction = 0F; 261 | getValueAnimator().addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 262 | @Override 263 | public void onAnimationUpdate(ValueAnimator animation) { 264 | float animatedFraction = animation.getAnimatedFraction(); 265 | if (lastAnimatedFraction == animatedFraction) return; 266 | lastAnimatedFraction = animatedFraction; 267 | if (updateListener != null) updateListener.onAnimationUpdate(animation); 268 | } 269 | }); 270 | getValueAnimator().start(); 271 | } 272 | 273 | @Override 274 | protected void onDetachedFromWindow() { 275 | super.onDetachedFromWindow(); 276 | if (getValueAnimator() != null && getValueAnimator().isRunning()) { 277 | getValueAnimator().cancel(); 278 | getValueAnimator().removeAllListeners(); 279 | getValueAnimator().removeAllUpdateListeners(); 280 | } 281 | 282 | } 283 | 284 | //获取图形背景色 285 | public int getBackgroundColor() { 286 | Drawable drawable = this.getBackground(); 287 | if (drawable instanceof ColorDrawable) { 288 | return ((ColorDrawable) drawable).getColor(); 289 | } 290 | return Color.WHITE; 291 | } 292 | 293 | public int getCharSequenceLength(CharSequence source) { 294 | return TextUtils.isEmpty(source) ? 0 : source.length(); 295 | } 296 | 297 | //得到 298 | public RectF getCharSequenceRect(CharSequence sequence) { 299 | if (sequence instanceof Spannable) { 300 | mLayout = new StaticLayout(sequence, getNotesPaint(), 10000, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0f, false); 301 | float textHeight = mLayout.getHeight(); 302 | float maxLineWidth = getMaxLineWidth(); 303 | return new RectF(0, 0, maxLineWidth, textHeight); 304 | } else { 305 | Paint.FontMetrics fontMetrics = getNotesPaint().getFontMetrics(); 306 | float textHeight = fontMetrics.descent - fontMetrics.ascent; 307 | float textWidth = getNotesPaint().measureText(sequence, 0, getCharSequenceLength(sequence)); 308 | return new RectF(0, 0, textWidth, textHeight); 309 | } 310 | 311 | } 312 | 313 | //获取所有行中字符最长的哪一行的宽度 314 | public float getMaxLineWidth() { 315 | int lineCount = mLayout.getLineCount(); 316 | float maxWidth = mLayout.getLineWidth(0); 317 | for (int i = 0; i < lineCount; i++) { 318 | Math.max(maxWidth, mLayout.getLineWidth(i)); 319 | } 320 | return maxWidth; 321 | 322 | } 323 | 324 | public RectF getLegendRect() { 325 | if (getLegend().getLegendRect() != null) 326 | return getLegend().getLegendRect(); 327 | 328 | List descriptions; 329 | descriptions = getLegend().getDescription(); 330 | if (descriptions == null || descriptions.size() == 0) { 331 | descriptions = new ArrayList<>(); 332 | for (int i = 0; i < getChildCount(); i++) { 333 | CharSequence sequence = mDatas.get(i).getCharSequence(); 334 | descriptions.add(sequence); 335 | } 336 | } 337 | 338 | if (descriptions == null || descriptions.size() == 0) 339 | return null; 340 | 341 | int position = getLegend().getPosition(); 342 | RectF paddingRectF = getLegend().getPaddingRectF(); 343 | 344 | float sumLegendWidth = 0F; 345 | float sumLegendHeight = 0F; 346 | 347 | float maxLegendWidth = 0F; 348 | float maxLegendHeight = 0F; 349 | 350 | float desLegendPad = getLegend().getDesLegendPad(); 351 | float legendPad = getLegend().getLegendPad(); 352 | float legendIndicatorWidth = getLegend().getLegendIndicatorWidthByStyle(); 353 | 354 | for (int i = 0; i < descriptions.size(); i++) { 355 | CharSequence charSequence = descriptions.get(i); 356 | RectF rectF = getCharSequenceRect(charSequence); 357 | float legendWidth = rectF.width() + desLegendPad + legendPad + legendIndicatorWidth; 358 | if (position == Legend.Position.TOP_RIGHT_VERTICAL) { 359 | if (i % Legend.TOP_VERTICAL_MAX_LEGEND == 0 && i > 0) {//3 0 1 2 0 360 | sumLegendWidth += maxLegendWidth; 361 | sumLegendHeight = Math.max(sumLegendHeight, maxLegendHeight); 362 | maxLegendHeight = 0; 363 | } 364 | sumLegendWidth = Math.max(sumLegendWidth, legendWidth); 365 | maxLegendHeight += rectF.height(); 366 | } else if (position == Legend.Position.TOP_RIGHT_HORIZONTAL) { 367 | if (i % Legend.TOP_HORIZONTAL_MAX_LEGEND == 0 && i > 0) {// 6 0 1 2 3 4 5 0 368 | sumLegendHeight += maxLegendHeight; 369 | sumLegendWidth = Math.max(sumLegendWidth, maxLegendWidth); 370 | maxLegendHeight = 0; 371 | maxLegendWidth = 0; 372 | } 373 | maxLegendWidth += legendWidth; 374 | maxLegendHeight = Math.max(maxLegendHeight, rectF.height()); 375 | 376 | } else if (position == Legend.Position.RIGHT) { 377 | if (i % Legend.RIGHT_MAX_LEGEND == 0 && i > 0) {//6 0 1 2 3 378 | sumLegendWidth += maxLegendWidth; 379 | sumLegendHeight = Math.max(sumLegendHeight, maxLegendHeight); 380 | maxLegendHeight = 0; 381 | } 382 | sumLegendWidth = Math.max(sumLegendWidth, legendWidth); 383 | maxLegendHeight += rectF.height(); 384 | } else if (position == Legend.Position.Bottom) { 385 | if (i % Legend.BOTTOM_MAX_LEGEND == 0 && i > 0) {// 6 0 1 2 3 4 5 0 386 | sumLegendHeight += maxLegendHeight; 387 | sumLegendWidth = Math.max(sumLegendWidth, maxLegendWidth); 388 | maxLegendHeight = 0; 389 | maxLegendWidth = 0; 390 | } 391 | maxLegendWidth += legendWidth; 392 | maxLegendHeight = Math.max(maxLegendHeight, rectF.height()); 393 | } 394 | } 395 | 396 | RectF rectF = new RectF(0, 0, sumLegendWidth, sumLegendHeight); 397 | getLegend().setLegendRect(rectF); 398 | return rectF; 399 | } 400 | 401 | public double scaleDouble(int newScale, double dv) { 402 | BigDecimal bg = new BigDecimal(dv); 403 | return bg.setScale(newScale, BigDecimal.ROUND_HALF_UP).doubleValue(); 404 | } 405 | 406 | //缩进矩形区域 407 | public void innerRect(float scale) { 408 | mRectF.set(mRectF.left + scale, mRectF.top + scale, mRectF.right - scale, mRectF.bottom - scale); 409 | } 410 | 411 | //放大矩形区域 412 | public void outRect(float scale) { 413 | mRectF.set(mRectF.left - scale, mRectF.top - scale, mRectF.right + scale, mRectF.bottom + scale); 414 | } 415 | 416 | //设置空状态文字 417 | public void setEmptyDescription(final CharSequence description) { 418 | mDescription = description; 419 | } 420 | 421 | public void drawDescription(Canvas canvas) { 422 | if (TextUtils.isEmpty(mDescription)) 423 | return; 424 | 425 | getNotesPaint().setTextSize(sp2px(12)); 426 | getNotesPaint().setColor(Color.GRAY); 427 | float desWidth = getNotesPaint().measureText(mDescription, 0, getCharSequenceLength(mDescription)); 428 | Paint.FontMetrics metrics = getNotesPaint().getFontMetrics(); 429 | float baseLine = getHeight() / 2 + ((metrics.bottom - metrics.top) / 2 - metrics.bottom) / 3; 430 | canvas.drawText(mDescription, 0, getCharSequenceLength(mDescription), getWidth() / 2 - desWidth / 2, baseLine, getNotesPaint()); 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /app/src/main/java/com/qiaomu/tablerow/chart/Legend.java: -------------------------------------------------------------------------------- 1 | package com.qiaomu.tablerow.chart; 2 | 3 | import android.graphics.RectF; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Created by qiaomu on 2017/7/31. 9 | */ 10 | public class Legend { 11 | 12 | 13 | //图例风格,举行,圆,线条 14 | public @interface Style { 15 | int RECT = 1; 16 | int CIRCLE = 2; 17 | int LINE = 3; 18 | } 19 | 20 | //图例位置--右上角垂直分布,水平分布,右边居中,底部居中 21 | public @interface Position { 22 | int TOP_RIGHT_VERTICAL = 0; 23 | int TOP_RIGHT_HORIZONTAL = 1; 24 | int RIGHT = 2; 25 | int Bottom = 3; 26 | } 27 | 28 | //换行方式 -两端对齐,居中对齐 29 | public @interface NewLine { 30 | int ALIGN_START = 1; 31 | int ALIGN_CENTER = 2; 32 | } 33 | 34 | public static final int TOP_VERTICAL_MAX_LEGEND = 2; 35 | public static final int TOP_HORIZONTAL_MAX_LEGEND = 6; 36 | public static final int RIGHT_MAX_LEGEND = 6; 37 | public static final int BOTTOM_MAX_LEGEND = 6; 38 | 39 | private List mColors; 40 | private List mDescription; 41 | private float mDesLegendPad = ChartConf.DEFAULT_PADDING; 42 | private float mRectWidth = ChartConf.DEFAULT_RECT_WIDTH; 43 | private float mRadius = ChartConf.DEFAULT_RECT_WIDTH; 44 | private float mLineWidth = ChartConf.DEFAULT_RECT_WIDTH; 45 | private float mLineHeight = ChartConf.DEFAULT_PADDING; 46 | private float mLegendPad = ChartConf.DEFAULT_RECT_WIDTH; 47 | private RectF mLegendRect; 48 | private RectF mPaddingRectF; 49 | @Style 50 | private int mLegendStyle = Style.RECT; 51 | @Position 52 | private int mPosition = Position.TOP_RIGHT_VERTICAL; 53 | @NewLine 54 | private int mNewLine = NewLine.ALIGN_CENTER; 55 | 56 | public List getColors() { 57 | return mColors; 58 | } 59 | 60 | public void setColors(List colors) { 61 | mColors = colors; 62 | } 63 | 64 | public List getDescription() { 65 | return mDescription; 66 | } 67 | 68 | public void setDescription(List description) { 69 | mDescription = description; 70 | } 71 | 72 | public float getDesLegendPad() { 73 | return mDesLegendPad; 74 | } 75 | 76 | public void setDesLegendPad(float desLegendPad) { 77 | mDesLegendPad = desLegendPad; 78 | } 79 | 80 | public float getRectWidth() { 81 | return mRectWidth; 82 | } 83 | 84 | public void setRectWidth(float rectWidth) { 85 | mRectWidth = rectWidth; 86 | } 87 | 88 | public float getRadius() { 89 | return mRadius; 90 | } 91 | 92 | public void setRadius(float radius) { 93 | mRadius = radius; 94 | } 95 | 96 | public float getLineWidth() { 97 | return mLineWidth; 98 | } 99 | 100 | public void setLineWidth(float lineWidth) { 101 | mLineWidth = lineWidth; 102 | } 103 | 104 | public int getLegendStyle() { 105 | return mLegendStyle; 106 | } 107 | 108 | public void setLegendStyle(int legendStyle) { 109 | mLegendStyle = legendStyle; 110 | } 111 | 112 | public int getPosition() { 113 | return mPosition; 114 | } 115 | 116 | public void setPosition(int position) { 117 | mPosition = position; 118 | } 119 | 120 | public int getNewLine() { 121 | return mNewLine; 122 | } 123 | 124 | public void setNewLine(int newLine) { 125 | mNewLine = newLine; 126 | } 127 | 128 | public float getLineHeight() { 129 | return mLineHeight; 130 | } 131 | 132 | public void setLineHeight(float lineHeight) { 133 | mLineHeight = lineHeight; 134 | } 135 | 136 | public void setPaddingRectF(float left, float top, float right, float bottom) { 137 | mPaddingRectF = new RectF(left, top, right, bottom); 138 | } 139 | 140 | public RectF getPaddingRectF() { 141 | return mPaddingRectF; 142 | } 143 | 144 | public float getLegendPad() { 145 | return mLegendPad; 146 | } 147 | 148 | public void setLegendPad(float legendPad) { 149 | mLegendPad = legendPad; 150 | } 151 | 152 | 153 | public float getLegendIndicatorWidthByStyle() { 154 | if (mLegendStyle == Style.RECT) 155 | return getRectWidth(); 156 | else if (mLegendStyle == Style.CIRCLE) 157 | return getRadius(); 158 | else return getLineWidth(); 159 | } 160 | public void setLegendRect(RectF legendRect) { 161 | mLegendRect = legendRect; 162 | } 163 | public RectF getLegendRect(){ 164 | return mLegendRect; 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /app/src/main/java/com/qiaomu/tablerow/chart/PieAnimation.java: -------------------------------------------------------------------------------- 1 | package com.qiaomu.tablerow.chart; 2 | 3 | import android.support.annotation.IntDef; 4 | 5 | /** 6 | * Created by qiaomu on 2017/7/21. 7 | */ 8 | @IntDef({PieAnimation.ONE_BY_ONE, 9 | PieAnimation.ONE_AND_ONE, 10 | PieAnimation.NONE}) 11 | public @interface PieAnimation { 12 | int ONE_BY_ONE = 0; //一个接一个 13 | int ONE_AND_ONE = 1; //所有的一起开始 14 | int NONE = 2; //没有 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/qiaomu/tablerow/chart/PieChart.java: -------------------------------------------------------------------------------- 1 | package com.qiaomu.tablerow.chart; 2 | 3 | import android.animation.ValueAnimator; 4 | import android.content.Context; 5 | import android.graphics.Canvas; 6 | import android.graphics.Paint; 7 | import android.graphics.Path; 8 | import android.graphics.PorterDuff; 9 | import android.graphics.PorterDuffXfermode; 10 | import android.graphics.RectF; 11 | import android.support.annotation.Nullable; 12 | import android.text.Layout; 13 | import android.text.Spanned; 14 | import android.text.StaticLayout; 15 | import android.util.AttributeSet; 16 | import android.util.Log; 17 | 18 | import java.util.Collections; 19 | import java.util.Comparator; 20 | import java.util.List; 21 | 22 | /** 23 | * Created by qiaomu on 2017/7/20. 24 | *

25 | * 写在前面: 26 | *

27 | * 1.设置数据唯一入口{@link PieChart#setDatas(List)} 28 | *

29 | * 2.进入动画具体查看{@link PieAnimation},默认是{@link PieChart#mAnimation} 30 | *

31 | * 3.扇形文字说明有三种位置具体查看{@link PiePosition},默认{@link PieEntry#mCharSequencePosition} 32 | *

33 | * 4.设置扇形阶梯等比放大{@link PieChart#setUseLevel(boolean, boolean, float)} } 34 | *

35 | * 5.其他更多可配置属性查看{@link PieEntry} 36 | *

37 | * 6.已知问题 38 | * --------目前只能从-90°开始绘制,也就是垂直方向,原因我还没看 39 | */ 40 | 41 | public class PieChart extends ChartView { 42 | public static float startAngle = -90F; 43 | private int mAnimation = PieAnimation.ONE_AND_ONE; 44 | 45 | private boolean mUseLevel; 46 | private boolean mInner; 47 | private float mLevelStep; 48 | 49 | private float fraction = 0F; 50 | private boolean mMinMode; 51 | 52 | private float lastOutY;//上一次绘制文本的位置 53 | private float lastOutX; 54 | 55 | public PieChart(Context context) { 56 | super(context); 57 | } 58 | 59 | public PieChart(Context context, @Nullable AttributeSet attrs) { 60 | super(context, attrs); 61 | } 62 | 63 | public PieChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 64 | super(context, attrs, defStyleAttr); 65 | } 66 | 67 | public void setDatas(List pieEntryList) { 68 | this.mDatas = pieEntryList; 69 | sortDatas(); 70 | resetParams(); 71 | calculateOffset(); 72 | makeChartRegion(); 73 | startValueAnimation(); 74 | } 75 | 76 | //最大 最小 次大次小排序 77 | private void sortDatas() { 78 | if (mDatas == null || mDatas.size() == 0) 79 | return; 80 | if (mDatas.size() > 2) { 81 | Collections.sort(mDatas, new Comparator() { 82 | @Override 83 | public int compare(PieEntry o1, PieEntry o2) { 84 | return ((Double) o1.getPercent()).compareTo((Double) o2.getPercent()); 85 | } 86 | }); 87 | for (int i = 0; i < mDatas.size() / 2; i++) { //0 3 1 2 88 | int index = i + i + 1; 89 | if (index < mDatas.size()) 90 | mDatas.add(index, mDatas.remove(mDatas.size() - i - 1)); 91 | else break; 92 | } 93 | } 94 | 95 | float start = startAngle; 96 | for (int i = 0; i < mDatas.size(); i++) {// 1 3 5 7 9 11 13 15 97 | PieEntry pieEntry = mDatas.get(i); 98 | pieEntry.setStartAngle(start); 99 | pieEntry.setSweepAngle(pieEntry.getPercent() * 360); 100 | start += pieEntry.getPercent() * 360; 101 | } 102 | 103 | } 104 | 105 | //画扇形之间的分割线 和指示器 106 | private void drawPieCharts(Canvas canvas) { 107 | //重新恢复矩形到初始状态 108 | // clearCanvas(); 109 | float curStartAngle = startAngle; 110 | for (int i = 0; i < getChildCount(); i++) {// 1 3 5 7 9 11 13 15 111 | PieEntry pieEntry = mDatas.get(i); 112 | getGraphPaint().setStyle(Paint.Style.FILL); 113 | getGraphPaint().setColor(pieEntry.getChartColor()); 114 | if (mAnimation == PieAnimation.ONE_BY_ONE) { 115 | double sweepPercent = pieEntry.getSweepAngle() * fraction; 116 | canvas.drawArc(mRectF, curStartAngle, (float) sweepPercent, true, getGraphPaint()); 117 | curStartAngle += sweepPercent; 118 | } else if (mAnimation == PieAnimation.ONE_AND_ONE) { 119 | canvas.drawArc(mRectF, (float) pieEntry.getStartAngle(), (float) pieEntry.getSweepAngle() * fraction, true, getGraphPaint()); 120 | } else { 121 | if (mUseLevel) { 122 | if (mInner && i > 0) { 123 | innerRect(mLevelStep); 124 | } else if (!mInner) { 125 | if (i == 0) 126 | innerRect(mLevelStep); 127 | else if (i == 1) 128 | outRect(mLevelStep); 129 | } 130 | } 131 | 132 | canvas.drawArc(mRectF, (float) pieEntry.getStartAngle(), (float) pieEntry.getSweepAngle(), true, getGraphPaint()); 133 | } 134 | 135 | } 136 | drawDecorations(canvas); 137 | } 138 | 139 | //绘制图形修饰属性 140 | 141 | private void drawDecorations(Canvas canvas) { 142 | //重新恢复矩形到初始状态 143 | if (mUseLevel) 144 | outRect((getChildCount() - 1) * 10); 145 | 146 | float totalSweepAngle4Divider = 0F; 147 | float totalSweepAngle4CharSequence = 0F; 148 | for (int i = 0; i < getChildCount(); i++) { 149 | //如果需要等绘制那么 矩形框的外边界要扩大 150 | if (mUseLevel) { 151 | if (mInner && i > 0) { 152 | innerRect(mLevelStep); 153 | } else if (!mInner) { 154 | if (i == 0) 155 | innerRect(mLevelStep); 156 | else if (i == 1) 157 | outRect(mLevelStep); 158 | } 159 | } 160 | totalSweepAngle4Divider = drawDivider(canvas, totalSweepAngle4Divider, mDatas.get(i)); 161 | totalSweepAngle4CharSequence = drawCharSequence(canvas, totalSweepAngle4CharSequence, mDatas.get(i)); 162 | 163 | } 164 | } 165 | 166 | //绘制分割线 167 | private float drawDivider(Canvas canvas, float sweepAngle, PieEntry pieEntry) { 168 | if (mAnimation == PieAnimation.ONE_BY_ONE && fraction < 1.0f) 169 | return 0; 170 | //如果就一个饼,或者分割线看度小于0 或者不显示说明文字 就不继续绘制分割线 || pieEntry.getCharSequencePosition() == PiePosition.INSIDE 171 | if (pieEntry.getSweepAngle() >= 360 || pieEntry.getSweepAngle() <= 0 || pieEntry.getDividerWidth() <= 0 || !pieEntry.isDisplayCharSequence()) { 172 | sweepAngle += pieEntry.getSweepAngle(); 173 | return sweepAngle; 174 | } 175 | if (pieEntry.getDividerColor() == -1) { 176 | getDividerPaint().setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 177 | //getDividerPaint().setColor(getBackgroundColor()); 178 | } else { 179 | getDividerPaint().setColor(pieEntry.getDividerColor()); 180 | } 181 | getDividerPaint().setStyle(Paint.Style.STROKE); 182 | getDividerPaint().setStrokeWidth(pieEntry.getDividerWidth()); 183 | 184 | canvas.save(); 185 | canvas.translate(mRectF.centerX(), mRectF.centerY()); 186 | 187 | Path path = new Path(); 188 | float moveToX = (float) Math.sin(Math.PI * sweepAngle / 180.0) * pieEntry.getDividerWidth(); 189 | float moveToY = -(float) Math.cos(Math.PI * sweepAngle / 180.0) * pieEntry.getDividerWidth(); 190 | path.moveTo(moveToX, moveToY); 191 | path.lineTo(0, 0); 192 | 193 | sweepAngle += pieEntry.getSweepAngle(); 194 | float startX = (float) Math.sin(Math.PI * sweepAngle / 180.0) * pieEntry.getDividerWidth(); 195 | float startY = -(float) Math.cos(Math.PI * sweepAngle / 180.0) * pieEntry.getDividerWidth(); 196 | path.lineTo(startX, startY); 197 | 198 | float endX = (float) Math.sin(Math.PI * sweepAngle / 180.0) * (mRectF.width() / 2 + 2); 199 | float endY = -(float) Math.cos(Math.PI * sweepAngle / 180.0) * (mRectF.width() / 2 + 2); 200 | path.lineTo(endX, endY); 201 | 202 | canvas.drawPath(path, getDividerPaint()); 203 | canvas.restore(); 204 | return sweepAngle; 205 | } 206 | 207 | //绘制图形化说明指示器 文字 208 | private float drawCharSequence(Canvas canvas, float middleSweepAngle, PieEntry pieEntry) { 209 | if (fraction < 1.0f) 210 | return 0; 211 | 212 | if (!pieEntry.isDisplayCharSequence() || pieEntry.getSweepAngle() <= 0) { 213 | middleSweepAngle += pieEntry.getSweepAngle(); 214 | return middleSweepAngle; 215 | } 216 | //绘制线条 指示器 217 | canvas.save(); 218 | canvas.translate(mRectF.centerX(), mRectF.centerY()); 219 | 220 | CharSequence source = pieEntry.getCharSequence(); 221 | RectF rectF = getCharSequenceRect(source); 222 | float textWidth = rectF.width(); 223 | float textHeight = rectF.height(); 224 | float baseLine = textHeight / 2; 225 | 226 | middleSweepAngle += pieEntry.getSweepAngle() / 2;//扇形扫过角度的一半 227 | boolean thanHalfX = middleSweepAngle > 180; 228 | boolean thanHalfY = middleSweepAngle >= 90 && middleSweepAngle <= 270; 229 | //指示器开始点x 230 | float startX = (float) Math.sin(Math.PI * middleSweepAngle / 180.0) * (mRectF.width() / 2 - pieEntry.getIndicatorLength()); 231 | //指示器开始点y 232 | float startY = -(float) (Math.cos(Math.PI * middleSweepAngle / 180.0) * (mRectF.width() / 2 - pieEntry.getIndicatorLength())); 233 | //指示器中间点x 234 | float middleX = (float) (Math.sin(Math.PI * middleSweepAngle / 180.0) * (mRectF.width() / 2 + pieEntry.getIndicatorLength())); 235 | //指示器中间点y 236 | float middleY = -(float) (Math.cos(Math.PI * middleSweepAngle / 180.0) * (mRectF.width() / 2 + pieEntry.getIndicatorLength())); 237 | 238 | 239 | //为了避免指示器水平方向上过长 240 | float overLength = Math.abs(Math.abs(middleX) - mRectF.width() / 2 - pieEntry.getIndicatorLength()); 241 | float endLength = 0; 242 | if (mMinMode) { 243 | endLength = thanHalfX ? (-Math.abs(pieEntry.getIndicatorLength() - overLength)) : (Math.abs(pieEntry.getIndicatorLength() - overLength)); 244 | } else { 245 | endLength = (middleSweepAngle == 90 || middleSweepAngle == 270) ? 0 : (thanHalfX ? -pieEntry.getIndicatorLength() : pieEntry.getIndicatorLength()); 246 | } 247 | 248 | if (pieEntry.getCharSequencePosition() != PiePosition.INSIDE && pieEntry.getIndicatorWidth() >= 1) { 249 | 250 | Path path = new Path(); 251 | getNotesPaint().setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)); 252 | getNotesPaint().setColor(pieEntry.getIndicatorColor()); 253 | getNotesPaint().setStrokeJoin(Paint.Join.ROUND); 254 | getNotesPaint().setStrokeCap(Paint.Cap.ROUND); 255 | getNotesPaint().setStyle(Paint.Style.STROKE); 256 | getNotesPaint().setStrokeWidth(pieEntry.getIndicatorWidth()); 257 | path.moveTo(startX, startY); 258 | path.lineTo(middleX, middleY); 259 | 260 | path.lineTo(middleX + endLength, middleY); 261 | getNotesPaint().setDither(true); 262 | getNotesPaint().setAntiAlias(true); 263 | canvas.drawPath(path, getNotesPaint()); 264 | } 265 | 266 | //绘制文字考虑位置 267 | getNotesPaint().reset(); 268 | getNotesPaint().setTextSize(pieEntry.getPieTextSize()); 269 | getNotesPaint().setColor(pieEntry.getCharSequenceColor()); 270 | 271 | if (!(source instanceof Spanned)) 272 | mLayout = new StaticLayout(source, getNotesPaint(), 10000, Layout.Alignment.ALIGN_NORMAL, pieEntry.getSpacingmult(), pieEntry.getSpacingadd(), false); 273 | 274 | if (pieEntry.getCharSequencePosition() == PiePosition.OUT_SIDE) { 275 | //大于180°,字体为止往左移,小于180°字体为止就是结束点x 276 | float outx = middleX + endLength + (thanHalfX ? -textWidth - pieEntry.getIndicatorCharPad() : pieEntry.getIndicatorCharPad()); 277 | float outy = middleY - baseLine; 278 | 279 | //校正,防止绘制出边界 280 | if (thanHalfX) 281 | outx = Math.max(-getWidth() / 2, outx); 282 | else 283 | outx = Math.min(getWidth() / 2 - textWidth - pieEntry.getIndicatorCharPad(), outx); 284 | canvas.translate(outx, outy); 285 | lastOutY = outy; 286 | lastOutX = outx; 287 | } else if (pieEntry.getCharSequencePosition() == PiePosition.INSIDE) { 288 | float insideX = (float) Math.sin(Math.PI * middleSweepAngle / 180.0) * mRectF.width() / 4; 289 | float insideY = -(float) Math.cos(Math.PI * middleSweepAngle / 180.0) * mRectF.width() / 4; 290 | canvas.translate(insideX - textWidth / 4, insideY - baseLine); 291 | } else if (pieEntry.getCharSequencePosition() == PiePosition.OUT_TOP) { 292 | float dstX = middleX; 293 | if (middleSweepAngle == 90 || middleSweepAngle == 270) { 294 | if (textWidth >= pieEntry.getIndicatorLength()) { 295 | if (thanHalfX) { 296 | dstX += -textWidth + pieEntry.getIndicatorLength() - pieEntry.getIndicatorCharPad(); 297 | } else { 298 | dstX += -pieEntry.getIndicatorLength() + pieEntry.getIndicatorCharPad(); 299 | } 300 | } else { 301 | if (thanHalfX) { 302 | dstX += pieEntry.getIndicatorLength() / 2 - textWidth / 2; 303 | } else { 304 | dstX += -pieEntry.getIndicatorLength() / 2 - textWidth / 2; 305 | } 306 | } 307 | } else { 308 | if (textWidth >= pieEntry.getIndicatorLength()) { 309 | if (thanHalfX) { 310 | dstX += -textWidth; 311 | } else { 312 | dstX = middleX; 313 | } 314 | } else { 315 | if (thanHalfX) { 316 | dstX += -pieEntry.getIndicatorLength() / 2 - textWidth / 2; 317 | } else { 318 | dstX += pieEntry.getIndicatorLength() / 2 - textWidth / 2; 319 | } 320 | } 321 | } 322 | float dstY = middleY - 1 * (textHeight + pieEntry.getIndicatorCharPad()); 323 | canvas.translate(dstX, dstY); 324 | } 325 | mLayout.draw(canvas); 326 | //累加扇形中间角度 327 | middleSweepAngle += pieEntry.getSweepAngle() / 2; 328 | canvas.restore(); 329 | return middleSweepAngle; 330 | } 331 | 332 | 333 | @Override 334 | public void calculateOffset() { 335 | int size = getChildCount(); 336 | for (int i = 0; i < size; i++) { 337 | 338 | PieEntry pieEntry = mDatas.get(i); 339 | //扇形中间角度 340 | middleSweepAngle += pieEntry.getSweepAngle() / 2; 341 | if (!pieEntry.isDisplayCharSequence()) 342 | continue; 343 | CharSequence source = pieEntry.getCharSequence(); 344 | getNotesPaint().setTextSize(pieEntry.getPieTextSize()); 345 | RectF rectF = getCharSequenceRect(source); 346 | float textWidth = rectF.width(); 347 | float textHeight = rectF.height(); 348 | 349 | if (pieEntry.getCharSequencePosition() == PiePosition.OUT_SIDE) { 350 | calculateMaxOffset(textWidth, textHeight, i, 1, pieEntry); 351 | } else if (pieEntry.getCharSequencePosition() == PiePosition.OUT_TOP) { 352 | //指示器长度与文本长度之差 353 | float diffTxtLength = Math.abs(pieEntry.getIndicatorLength() - textWidth); 354 | calculateMaxOffset(diffTxtLength, textHeight, i, 2, pieEntry); 355 | } 356 | middleSweepAngle += pieEntry.getSweepAngle() / 2; 357 | 358 | } 359 | } 360 | 361 | float indicatorCharPad = 0F; 362 | float indicatorLength = 0F; 363 | float middleSweepAngle = 0F; 364 | float maxTxtLength = 0F; 365 | 366 | private void calculateMaxOffset(float textLength, float textHeight, int index, int position, PieEntry pieEntry) { 367 | //取指示器长度与文本长度之差得最大值 368 | maxTxtLength = Math.max(maxTxtLength, textLength); 369 | //取指示器与字体之间的最大值 370 | indicatorCharPad = Math.max(indicatorCharPad, pieEntry.getIndicatorCharPad()); 371 | //取只是其长度最大值 372 | indicatorLength = Math.max(pieEntry.getIndicatorLength(), indicatorLength); 373 | //原始圆形半径 374 | float originalRadius = Math.min(getWidth() / 2, getHeight() / 2); 375 | //扇形指示器方向半径 376 | float angleRadius = originalRadius + indicatorLength; 377 | //每个扇形指示器方向的半径投影到水平方向的长度 378 | double angleRadiusLengthHori = Math.abs(Math.sin(Math.PI * middleSweepAngle / 180.0) * angleRadius); 379 | //每个扇形指示器方向的半径投影到垂直方向的长度 380 | double angleRadiusLengthVertical = Math.abs(Math.cos(Math.PI * middleSweepAngle / 180.0) * angleRadius); 381 | 382 | if (position == PiePosition.OUT_SIDE) { 383 | angleRadiusLengthVertical += (mUseLevel ? (index + 1) * mLevelStep : 0) + textHeight / 2; 384 | double overLength = angleRadiusLengthHori - originalRadius; 385 | angleRadiusLengthHori += textLength + pieEntry.getIndicatorCharPad() + (mMinMode ? pieEntry.getIndicatorLength() - overLength : pieEntry.getIndicatorLength()); 386 | 387 | } else if (position == PiePosition.OUT_TOP) { 388 | angleRadiusLengthVertical += (mUseLevel ? (index + 1) * mLevelStep + pieEntry.getIndicatorCharPad() : 0) + pieEntry.getIndicatorWidth() + textHeight; 389 | angleRadiusLengthHori += textLength + pieEntry.getIndicatorCharPad() + pieEntry.getIndicatorLength(); 390 | } 391 | 392 | //水平方向上 需要流出的额外空间 393 | if (angleRadiusLengthHori > originalRadius) { 394 | mExtraLeftPad = (float) Math.max(angleRadiusLengthHori - originalRadius, mExtraLeftPad); 395 | } 396 | //顶部留出的额外空间=大于半径部分+字体高度+指示器宽度+字体与指示器间隔 397 | if (angleRadiusLengthVertical >= originalRadius) { 398 | if (middleSweepAngle < 90 || middleSweepAngle > 270) { 399 | double topPad = angleRadiusLengthVertical - originalRadius; 400 | mExtraTopPad = (float) Math.max(topPad, mExtraTopPad); 401 | } else if (middleSweepAngle > 90 || middleSweepAngle < 270) { 402 | double botPad = angleRadiusLengthVertical - originalRadius; 403 | mExtraBottomPad = (float) Math.max(botPad, mExtraBottomPad); 404 | } 405 | } 406 | Log.e(TAG, "calculateMaxOffset: " + middleSweepAngle + "--" + angleRadiusLengthVertical + "--" + mExtraTopPad); 407 | } 408 | 409 | @Override 410 | public void makeChartRegion() { 411 | //通过先求得垂直方向半径 412 | float radius = (getHeight() - getRectTop() - getExtraPaddingBottom() - getPaddingBottom()) / 2; 413 | //求得水平方向左便宜量 414 | float left = getWidth() / 2 - radius; 415 | if (getWidth() / 2 - radius < getRectLeft()) { 416 | left = getRectLeft(); 417 | float realRadius = getWidth() / 2 - left; 418 | mExtraTopPad += radius - realRadius; 419 | mExtraBottomPad += radius - realRadius; 420 | radius = realRadius; 421 | } 422 | 423 | mRectF = new RectF(left, getRectTop(), left + 2 * radius, getHeight() - getExtraPaddingBottom() - getPaddingBottom()); 424 | // Log.e(TAG, "makeChartRegion: " + mExtraLeftPad + "--" + mExtraBottomPad + "--" + mExtraBottomPad); 425 | } 426 | 427 | @Override 428 | public void render(Canvas canvas) { 429 | drawPieCharts(canvas); 430 | } 431 | 432 | public void setPieChartAnimation(@PieAnimation int animation) { 433 | mAnimation = animation; 434 | } 435 | 436 | //使用分级 437 | public void setUseLevel(boolean useLevel, boolean inner, float levelStep) { 438 | mUseLevel = useLevel; 439 | mInner = inner; 440 | mLevelStep = dp2px(levelStep); 441 | if (mUseLevel) 442 | setPieChartAnimation(PieAnimation.NONE); 443 | } 444 | 445 | //宽高比较小的控件上使用适配更好 446 | public void setMinMode(boolean midMode) { 447 | mMinMode = midMode; 448 | } 449 | 450 | private void startValueAnimation() { 451 | if (mAnimation == PieAnimation.NONE) { 452 | fraction = 1f; 453 | invalidate(); 454 | return; 455 | } 456 | startValueAnimation(new ValueAnimator.AnimatorUpdateListener() { 457 | @Override 458 | public void onAnimationUpdate(ValueAnimator animation) { 459 | fraction = animation.getAnimatedFraction(); 460 | invalidate(); 461 | } 462 | }); 463 | } 464 | 465 | private void resetParams() { 466 | fraction = 0f; 467 | indicatorCharPad = 0F; 468 | indicatorLength = 0F; 469 | middleSweepAngle = 0F; 470 | maxTxtLength = 0F; 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /app/src/main/java/com/qiaomu/tablerow/chart/PieEntry.java: -------------------------------------------------------------------------------- 1 | package com.qiaomu.tablerow.chart; 2 | 3 | import android.graphics.Color; 4 | import android.support.annotation.ColorInt; 5 | import android.support.annotation.FloatRange; 6 | 7 | /** 8 | * Created by qiaomu on 2017/7/20. 9 | *

10 | * 这是饼图的每个扇形的实体类,每个字段的意思有必要大致看一下,很多属性都是可配置的 11 | */ 12 | 13 | public class PieEntry extends BaseEntry { 14 | private int DEFAULT_COLOR = ChartConf.DEFAULT_COLOR; 15 | /** 16 | * 每个扇形描述文字大小 17 | */ 18 | private float mPieTextSize = ChartConf.DEFAULT_SUB_SIZE; 19 | /** 20 | * 每个扇形的颜色 21 | */ 22 | private int mChartColor = DEFAULT_COLOR; 23 | /** 24 | * 每个扇形指示器颜色 25 | */ 26 | private int mIndicatorColor = Color.RED; 27 | /** 28 | * 每个扇形的指示器宽度 29 | */ 30 | private int mIndicatorWidth = ChartConf.DEFAULT_STROKE_WIDTH; 31 | /** 32 | * 每个扇形的指示器长度,假设你指定了5dp,那么将有5dp位于扇形内,非垂直或水平方向的将有10dp位于扇形外,垂直或水平将有5dp位于扇形外 33 | */ 34 | private float mIndicatorLength = ChartConf.DEFAULT_LENGTH10dp; 35 | /** 36 | * 扇形指示器与文字描述之间的间距 37 | */ 38 | private float mIndicatorCharPad = ChartConf.DEFAULT_PADDING; 39 | 40 | /** 41 | * 扇形描述性文字颜色 42 | */ 43 | private int mCharSequenceColor = DEFAULT_COLOR; 44 | //暂时用不到 45 | private float spacingmult = ChartConf.DEFAULT_SPACINGMULT; 46 | private float spacingadd = ChartConf.DEFAULT_SPACINGADD; 47 | 48 | /** 49 | * 必传,该扇形在饼图中所占百分比 50 | */ 51 | private double mPercent; 52 | /** 53 | * 扇形开始绘制角度 54 | */ 55 | private double mStartAngle; 56 | /** 57 | * 扇形扫过角度 58 | */ 59 | private double mSweepAngle; 60 | /** 61 | * 是否需要显示描述文字,如果不显示那么指示器也是一同不显示的 62 | */ 63 | private boolean mDisplayCharSequence = true; 64 | /** 65 | * 扇形之间的分割线的宽度,传0不显示 66 | */ 67 | private float mDividerWidth = ChartConf.DEFAULT_STROKE_WIDTH; 68 | /** 69 | * 扇形之间的分割线的颜色,如果mDividerWidth大于0,但是不设置分割线颜色,那么显示将是你设置的控件背景颜色 70 | */ 71 | private int mDividerColor = -1; 72 | /** 73 | * 指示器文字说明的位置 74 | */ 75 | @PiePosition 76 | private int mCharSequencePosition = PiePosition.OUT_SIDE; 77 | 78 | public PieEntry(@FloatRange(from = 0.0f, to = 1f) double percent, CharSequence charSequence) { 79 | setPercent(percent); 80 | setCharSequence(charSequence); 81 | } 82 | 83 | public int getChartColor() { 84 | return mChartColor; 85 | } 86 | 87 | public void setChartColor(@ColorInt int chartColor) { 88 | mChartColor = chartColor; 89 | setIndicatorColor(mChartColor); 90 | } 91 | 92 | public int getIndicatorColor() { 93 | return mIndicatorColor; 94 | } 95 | 96 | public void setIndicatorColor(@ColorInt int indicatorColor) { 97 | mIndicatorColor = indicatorColor; 98 | } 99 | 100 | public int getIndicatorWidth() { 101 | return mIndicatorWidth; 102 | } 103 | 104 | public void setIndicatorWidth(int indicatorWidth) { 105 | mIndicatorWidth = indicatorWidth; 106 | } 107 | 108 | private void setPercent(@FloatRange(from = 0.0f, to = 100.0f) double percent) { 109 | mPercent = percent; 110 | } 111 | 112 | public double getPercent() { 113 | return mPercent; 114 | } 115 | 116 | public float getIndicatorLength() { 117 | return mIndicatorLength; 118 | } 119 | 120 | public void setIndicatorLength(float indicatorLength) { 121 | mIndicatorLength = indicatorLength; 122 | } 123 | 124 | public float getSpacingmult() { 125 | return spacingmult; 126 | } 127 | 128 | public void setSpacingmult(float spacingmult) { 129 | this.spacingmult = spacingmult; 130 | } 131 | 132 | public float getSpacingadd() { 133 | return spacingadd; 134 | } 135 | 136 | public void setSpacingadd(float spacingadd) { 137 | this.spacingadd = spacingadd; 138 | } 139 | 140 | public float getPieTextSize() { 141 | return mPieTextSize; 142 | } 143 | 144 | public void setPieTextSize(float pieTextSize) { 145 | this.mPieTextSize = pieTextSize; 146 | } 147 | 148 | public void setDisplayCharSequence(boolean displayCharSequence) { 149 | this.mDisplayCharSequence = displayCharSequence; 150 | } 151 | 152 | public boolean isDisplayCharSequence() { 153 | return mDisplayCharSequence; 154 | } 155 | 156 | public double getStartAngle() { 157 | return mStartAngle; 158 | } 159 | 160 | public void setStartAngle(@FloatRange(from = -90.0f, to = 360.0f) double startAngle) { 161 | mStartAngle = startAngle; 162 | } 163 | 164 | public double getSweepAngle() { 165 | return mSweepAngle; 166 | } 167 | 168 | public void setSweepAngle(@FloatRange(from = 0.0f, to = 360.0f) double sweepAngle) { 169 | mSweepAngle = sweepAngle; 170 | } 171 | 172 | public void setDividerWidth(float dividerWidth) { 173 | mDividerWidth = dividerWidth; 174 | } 175 | 176 | public float getDividerWidth() { 177 | return mDividerWidth; 178 | } 179 | 180 | public void setDividerColor(@ColorInt int dividerColor) { 181 | mDividerColor = dividerColor; 182 | } 183 | 184 | public int getDividerColor() { 185 | return mDividerColor; 186 | } 187 | 188 | 189 | public float getIndicatorCharPad() { 190 | return mIndicatorCharPad; 191 | } 192 | 193 | public void setIndicatorCharPad(float indicatorCharPad) { 194 | mIndicatorCharPad = indicatorCharPad; 195 | } 196 | 197 | public void setCharSequencePosition(@PiePosition int charSequencePosition) { 198 | mCharSequencePosition = charSequencePosition; 199 | } 200 | 201 | public int getCharSequencePosition() { 202 | return mCharSequencePosition; 203 | } 204 | 205 | public int getCharSequenceColor() { 206 | return mCharSequenceColor; 207 | } 208 | 209 | public void setCharSequenceColor(@ColorInt int color) { 210 | this.mCharSequenceColor = color; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /app/src/main/java/com/qiaomu/tablerow/chart/PiePosition.java: -------------------------------------------------------------------------------- 1 | package com.qiaomu.tablerow.chart; 2 | 3 | import android.support.annotation.IntDef; 4 | 5 | /** 6 | * Created by qiaomu on 2017/7/21. 7 | */ 8 | @IntDef({PiePosition.INSIDE, PiePosition.OUT_SIDE, PiePosition.OUT_TOP}) 9 | public @interface PiePosition { 10 | int INSIDE = 0; //图形里面 11 | int OUT_SIDE = 1;//外面——线条外面 12 | int OUT_TOP = 2; //外面——线条上面 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/qiaomu/tablerow/view/MyListView.java: -------------------------------------------------------------------------------- 1 | package com.qiaomu.tablerow.view; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.util.Log; 6 | import android.widget.ListView; 7 | 8 | 9 | /** 10 | * Created by qiaomu on 2017/9/11. 11 | */ 12 | 13 | public class MyListView extends ListView { 14 | private static final String TAG = "MyListView"; 15 | 16 | public MyListView(Context context) { 17 | super(context); 18 | } 19 | 20 | public MyListView(Context context, AttributeSet attrs) { 21 | super(context, attrs); 22 | } 23 | 24 | public MyListView(Context context, AttributeSet attrs, int defStyleAttr) { 25 | super(context, attrs, defStyleAttr); 26 | } 27 | 28 | @Override 29 | protected void onScrollChanged(int l, int t, int oldl, int oldt) { 30 | super.onScrollChanged(l, t, oldl, oldt); 31 | } 32 | 33 | @Override 34 | public void scrollTo(int x, int y) { 35 | super.scrollTo(x, y); 36 | Log.e(TAG, "scrollTo: " + y); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 23 | 24 | 25 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_phone.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 |