├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── CircleLayout ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── nelson │ │ └── circlelayout │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── nelson │ │ │ └── circlelayout │ │ │ └── CircleLayout.java │ └── res │ │ └── values │ │ ├── attrs.xml │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── nelson │ └── circlelayout │ └── ExampleUnitTest.java ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── nelson │ │ └── circlelayoutdemo │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── nelson │ │ │ └── circlelayoutdemo │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable │ │ ├── center.png │ │ ├── down.png │ │ └── up.png │ │ ├── layout │ │ └── activity_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 │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── nelson │ └── circlelayoutdemo │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── picture └── circlelayout.gif └── 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/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CircleLayout/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /CircleLayout/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.novoda.bintray-release'//添加 3 | 4 | android { 5 | compileSdkVersion 25 6 | buildToolsVersion "25.0.2" 7 | 8 | defaultConfig { 9 | minSdkVersion 19 10 | targetSdkVersion 25 11 | versionCode 1 12 | versionName "1.0" 13 | 14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 15 | 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | tasks.withType(Javadoc) { 25 | options.addStringOption('Xdoclint:none', '-quiet') 26 | options.addStringOption('encoding', 'UTF-8') 27 | options.addStringOption('charSet', 'UTF-8') 28 | } 29 | } 30 | 31 | publish { 32 | userOrg = 'nelson1110'//bintray.com用户名 33 | groupId = 'com.nelson'//jcenter上的路径 34 | artifactId = 'CircleLayout'//项目名称 35 | publishVersion = '0.1.1'//版本号 36 | desc = 'a layout which can layout childs for circle'//描述,不重要 37 | website = 'https://github.com/nelson1110/CircleLayout'//网站,不重要;尽量模拟github上的地址,例如我这样的;当然你有地址最好了 38 | } 39 | 40 | dependencies { 41 | compile fileTree(dir: 'libs', include: ['*.jar']) 42 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 43 | exclude group: 'com.android.support', module: 'support-annotations' 44 | }) 45 | compile 'com.android.support:appcompat-v7:25.3.1' 46 | testCompile 'junit:junit:4.12' 47 | } 48 | -------------------------------------------------------------------------------- /CircleLayout/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 D:\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 | -------------------------------------------------------------------------------- /CircleLayout/src/androidTest/java/com/nelson/circlelayout/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.nelson.circlelayout; 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.nelson.circlelayout.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /CircleLayout/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /CircleLayout/src/main/java/com/nelson/circlelayout/CircleLayout.java: -------------------------------------------------------------------------------- 1 | package com.nelson.circlelayout; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.support.annotation.NonNull; 6 | import android.support.v4.util.Pair; 7 | import android.support.v4.view.GestureDetectorCompat; 8 | import android.support.v4.view.MotionEventCompat; 9 | import android.util.AttributeSet; 10 | import android.view.GestureDetector; 11 | import android.view.MotionEvent; 12 | import android.view.View; 13 | import android.view.ViewConfiguration; 14 | import android.view.ViewGroup; 15 | import android.view.ViewParent; 16 | 17 | 18 | /** 19 | * Created by Nelson on 2017/4/18. 20 | * 21 | */ 22 | 23 | public class CircleLayout extends ViewGroup implements GestureDetector.OnGestureListener { 24 | 25 | private final int mTouchSlop; 26 | private final int mMinimumVelocity; 27 | private final int mMaximumVelocity; 28 | private double mMinimumCornerVelocity; 29 | 30 | private int mRadius = 250;//子item的中心和整个layout中心的距离 31 | private int mMaxWidth = Integer.MAX_VALUE; 32 | private int mMaxHeight = Integer.MAX_VALUE; 33 | private GestureDetectorCompat mDetector; 34 | 35 | private int mCenterX; 36 | private int mCenterY; 37 | private double mChangeCorner = 0.0; 38 | private Pair mStart; 39 | private boolean isCanScroll = false; 40 | private boolean isDragging = false; 41 | private FlingRunnable mFlingRunnable; 42 | 43 | private View mCenterView; 44 | 45 | private float lastX; 46 | private float lastY; 47 | private boolean isFling; 48 | 49 | private Pair beforeFling; 50 | 51 | 52 | public CircleLayout(Context context) { 53 | this(context, null); 54 | } 55 | 56 | public CircleLayout(Context context, AttributeSet attrs) { 57 | this(context, attrs, 0); 58 | } 59 | 60 | public CircleLayout(Context context, AttributeSet attrs, int defStyleAttr) { 61 | super(context, attrs, defStyleAttr); 62 | mDetector = new GestureDetectorCompat(context, this); 63 | 64 | final ViewConfiguration configuration = ViewConfiguration.get(context); 65 | mTouchSlop = configuration.getScaledPagingTouchSlop(); 66 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 67 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 68 | final TypedArray a = context.obtainStyledAttributes( 69 | attrs, R.styleable.CircleLayout, defStyleAttr, defStyleAttr); 70 | if (attrs != null) { 71 | try { 72 | mRadius = (int) a.getDimension(R.styleable.CircleLayout_radium, 250); 73 | mChangeCorner = (double)a.getFloat(R.styleable.CircleLayout_changeCorner,0); 74 | } finally { 75 | a.recycle(); 76 | } 77 | 78 | } 79 | 80 | 81 | } 82 | 83 | 84 | @Override 85 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 86 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 87 | 88 | int childCount = getChildCount();//item的数量 89 | 90 | //可用宽高 91 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 92 | int heightSize = MeasureSpec.getSize(heightMeasureSpec); 93 | 94 | int ps = getPaddingStart(); 95 | int pe = getPaddingEnd(); 96 | int pt = getPaddingTop(); 97 | int pb = getPaddingBottom(); 98 | 99 | setMeasuredDimension(widthSize, heightSize); 100 | 101 | //子view最高多少,最宽多少 102 | int childMaxWidth = 0; 103 | int childMaxHeight = 0; 104 | View child; 105 | for (int i = 0; i < childCount; i++) { 106 | child = getChildAt(i); 107 | if (child.getVisibility() == GONE) { 108 | continue; 109 | } 110 | child.measure(MeasureSpec.makeMeasureSpec(widthSize - ps - pe, MeasureSpec.UNSPECIFIED) 111 | , MeasureSpec.makeMeasureSpec(heightSize - pt - pb, MeasureSpec.UNSPECIFIED)); 112 | 113 | childMaxWidth = Math.max(childMaxWidth, child.getMeasuredWidth()); 114 | childMaxHeight = Math.max(childMaxHeight, child.getMeasuredHeight()); 115 | } 116 | 117 | int width = resolveAdjustedSize(mRadius * 2 + childMaxWidth + ps + pe, mMaxWidth, widthMeasureSpec); 118 | int height = resolveAdjustedSize(mRadius * 2 + childMaxHeight + pt + pb, mMaxHeight, heightMeasureSpec); 119 | 120 | int finalWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec)); 121 | int finalHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec)); 122 | setMeasuredDimension(finalWidthSpec, finalHeightSpec); 123 | 124 | } 125 | 126 | private int resolveAdjustedSize(int desiredSize, int maxSize, 127 | int measureSpec) { 128 | int result = desiredSize; 129 | final int specMode = MeasureSpec.getMode(measureSpec); 130 | final int specSize = MeasureSpec.getSize(measureSpec); 131 | switch (specMode) { 132 | case MeasureSpec.UNSPECIFIED: 133 | /* Parent says we can be as big as we want. Just don't be larger 134 | than max size imposed on ourselves. 135 | */ 136 | result = Math.min(desiredSize, maxSize); 137 | break; 138 | case MeasureSpec.AT_MOST: 139 | // Parent says we can be as big as we want, up to specSize. 140 | // Don't be larger than specSize, and don't be larger than 141 | // the max size imposed on ourselves. 142 | result = Math.min(Math.min(desiredSize, specSize), maxSize); 143 | break; 144 | case MeasureSpec.EXACTLY: 145 | // No choice. Do what we are told. 146 | result = specSize; 147 | break; 148 | } 149 | return result; 150 | } 151 | 152 | @Override 153 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 154 | 155 | int childCount = mCenterView == null ? getChildCount() : getChildCount() - 1;//item的数量 156 | mCenterX = (getMeasuredWidth() - getPaddingStart() - getPaddingEnd()) / 2; 157 | mCenterY = (getMeasuredHeight() - getPaddingBottom() - getPaddingTop()) / 2; 158 | 159 | View child; 160 | int childWidth;//item的宽 161 | int childHeight;//item的高 162 | double corner;//旋转角度 163 | 164 | for (int i = 0; i < childCount; i++) { 165 | child = getChildAt(i); 166 | 167 | if (child.getVisibility() == GONE) { 168 | continue; 169 | } 170 | corner = 360 / childCount * i; 171 | 172 | childWidth = child.getMeasuredWidth(); 173 | childHeight = child.getMeasuredHeight(); 174 | 175 | 176 | int cX = (int) (mCenterX - mRadius * Math.cos(Math.toRadians(corner + mChangeCorner))); 177 | int cY = (int) (mCenterY - mRadius * Math.sin(Math.toRadians(corner + mChangeCorner))); 178 | 179 | child.layout(cX - childWidth / 2, cY - childHeight / 2, cX + childWidth / 2, cY + childHeight / 2); 180 | 181 | } 182 | if (mCenterView != null) { 183 | mCenterView.layout(mCenterX - mCenterView.getMeasuredWidth() / 2, mCenterY - mCenterView.getMeasuredHeight() / 2 184 | , mCenterX + mCenterView.getMeasuredWidth() / 2, mCenterY + mCenterView.getMeasuredHeight() / 2); 185 | } 186 | 187 | } 188 | 189 | @Override 190 | public boolean onInterceptTouchEvent(MotionEvent ev) { 191 | final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 192 | 193 | if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 194 | return false; 195 | } 196 | 197 | switch (action) { 198 | 199 | case MotionEvent.ACTION_DOWN: 200 | lastX = ev.getX(); 201 | lastY = ev.getY(); 202 | 203 | mStart = null; 204 | if (mFlingRunnable != null) { 205 | mFlingRunnable.endFling(); 206 | } 207 | if (isFling) { 208 | ViewParent parent = getParent(); 209 | if (parent != null) { 210 | parent.requestDisallowInterceptTouchEvent(true); 211 | } 212 | return true; 213 | } 214 | break; 215 | case MotionEvent.ACTION_MOVE: 216 | float x = ev.getX(); 217 | float y = ev.getY(); 218 | isDragging = Math.sqrt(Math.pow((x - lastX), 2) + Math.pow((y - lastY), 2)) > mTouchSlop; 219 | return isDragging; 220 | 221 | 222 | } 223 | return super.onInterceptTouchEvent(ev); 224 | } 225 | 226 | 227 | @Override 228 | public boolean onTouchEvent(MotionEvent event) { 229 | 230 | return (isCanScroll || isFling) && mDetector.onTouchEvent(event); 231 | } 232 | 233 | @SuppressWarnings("unused") 234 | public int getRadius() { 235 | return mRadius; 236 | } 237 | 238 | @SuppressWarnings("unused") 239 | public void setRadius(int mRadius) { 240 | this.mRadius = mRadius; 241 | requestLayout(); 242 | } 243 | 244 | @SuppressWarnings("unused") 245 | public int getMaxWidth() { 246 | return mMaxWidth; 247 | } 248 | 249 | @SuppressWarnings("unused") 250 | public void setMaxWidth(int mMaxWidth) { 251 | this.mMaxWidth = mMaxWidth; 252 | } 253 | 254 | @SuppressWarnings("unused") 255 | public int getMaxHeight() { 256 | return mMaxHeight; 257 | } 258 | 259 | @SuppressWarnings("unused") 260 | public void setMaxHeight(int mMaxHeight) { 261 | this.mMaxHeight = mMaxHeight; 262 | } 263 | 264 | @Override 265 | public boolean onDown(MotionEvent e) { 266 | return true; 267 | } 268 | 269 | @Override 270 | public void onShowPress(MotionEvent e) { 271 | 272 | } 273 | 274 | @Override 275 | public boolean onSingleTapUp(MotionEvent e) { 276 | return false; 277 | } 278 | 279 | @Override 280 | public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 281 | 282 | if (mStart == null) { 283 | mStart = new Pair<>(e2.getX() - mCenterX, e2.getY() - mCenterY); 284 | } 285 | 286 | Pair end = new Pair<>(e2.getX() - mCenterX, e2.getY() - mCenterY);//结束向量 287 | //角度 288 | Double changeCorner = Math.toDegrees(Math.acos((mStart.first * end.first + mStart.second * end.second) / (Math.sqrt(mStart.first * mStart.first + 289 | mStart.second * mStart.second) * Math.sqrt(end.first * end.first + end.second * end.second)))); 290 | 291 | //方向 >0 为顺时针 <0 为逆时针 292 | double changeDirection = mStart.first * end.second - mStart.second * end.first; 293 | 294 | if (!changeCorner.isNaN()) { 295 | if (changeDirection > 0) { 296 | mChangeCorner = (mChangeCorner + changeCorner) % 360; 297 | } else if (changeDirection < 0) { 298 | mChangeCorner = (mChangeCorner - changeCorner) % 360; 299 | } 300 | } 301 | 302 | requestLayout(); 303 | beforeFling = new Pair<>(mStart.first, mStart.second); 304 | mStart = end; 305 | return true; 306 | } 307 | 308 | @Override 309 | public void onLongPress(MotionEvent e) { 310 | } 311 | 312 | @Override 313 | public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 314 | if (e1 == null) { 315 | return false; 316 | } 317 | isFling = true; 318 | 319 | //合速度 320 | double v = Math.min(Math.sqrt(Math.pow(velocityX, 2) + Math.pow(velocityY, 2)), mMaximumVelocity); 321 | 322 | //三边长 323 | double oe1 = Math.sqrt(Math.pow(beforeFling.first, 2) + Math.pow(beforeFling.second, 2)); 324 | double oe2 = Math.sqrt(Math.pow(e2.getX() - mCenterX, 2) + Math.pow(e2.getY() - mCenterY, 2)); 325 | double e1e2 = Math.sqrt(Math.pow(e2.getX() - e1.getX(), 2) + Math.pow(e2.getY() - e1.getY(), 2)); 326 | 327 | double sin = Math.sqrt(Math.pow(1 - (Math.pow(oe2, 2) + Math.pow(e1e2, 2) - Math.pow(oe1, 2)) / (2 * oe2 * e1e2), 2)); 328 | //角速度 329 | double vc = 180 * v * sin / (Math.PI * oe2); 330 | //最小角速度 331 | mMinimumCornerVelocity = 180 * mMinimumVelocity * sin / (Math.PI * oe2); 332 | 333 | Pair end = new Pair<>(e2.getX() - mCenterX, e2.getY() - mCenterY);//结束向量 334 | 335 | //方向 >0 为顺时针 <0 为逆时针 336 | double flingDirection = beforeFling.first * end.second - beforeFling.second * end.first; 337 | 338 | if (mFlingRunnable != null) { 339 | removeCallbacks(mFlingRunnable); 340 | } 341 | 342 | post(mFlingRunnable = new FlingRunnable(flingDirection > 0 ? vc : -vc)); 343 | return true; 344 | } 345 | 346 | private class FlingRunnable implements Runnable { 347 | double v;//初始速度 348 | 349 | FlingRunnable(double v) { 350 | this.v = v; 351 | } 352 | 353 | @Override 354 | public void run() { 355 | if (Math.abs(v) >= mMinimumCornerVelocity) { 356 | // Keep the fling alive a little longer 357 | v /= 1.0666F; 358 | mChangeCorner = (mChangeCorner + v / 1000 * 16) % 360; 359 | postDelayed(this, 16); 360 | requestLayout(); 361 | } else { 362 | endFling(); 363 | } 364 | 365 | } 366 | 367 | private void endFling() { 368 | isFling = false; 369 | removeCallbacks(this); 370 | } 371 | } 372 | 373 | @SuppressWarnings("unused") 374 | public boolean isCanScroll() { 375 | return isCanScroll; 376 | } 377 | 378 | /** 379 | * @param canScroll 设置是否可以旋转 380 | */ 381 | public void setCanScroll(boolean canScroll) { 382 | isCanScroll = canScroll; 383 | } 384 | 385 | @SuppressWarnings("unused") 386 | public boolean isDragging() { 387 | return isDragging; 388 | } 389 | 390 | public void setCenterView(@NonNull View view) { 391 | if (mCenterView == null) { 392 | mCenterView = view; 393 | addView(mCenterView); 394 | } 395 | requestLayout(); 396 | } 397 | 398 | public void removeCenterView() { 399 | if (mCenterView != null) { 400 | removeView(mCenterView); 401 | mCenterView = null; 402 | } 403 | } 404 | 405 | /** 406 | * @return 获取中心的view,没有的话就返回null 407 | */ 408 | public View getCenterView(){ 409 | return mCenterView; 410 | } 411 | 412 | /** 413 | * @return 获取旋转角度 414 | */ 415 | public double getChangeCorner() { 416 | return mChangeCorner; 417 | } 418 | 419 | /** 420 | * @param changeCorner 设置旋转角度 421 | */ 422 | public void setChangeCorner(double changeCorner) { 423 | this.mChangeCorner = mChangeCorner; 424 | invalidate(); 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /CircleLayout/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /CircleLayout/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CircleLayout 3 | 4 | -------------------------------------------------------------------------------- /CircleLayout/src/test/java/com/nelson/circlelayout/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.nelson.circlelayout; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CircleLayout 2 | 环形布局,可拖动,独立item设置,可设置中心view 3 | 更自然,更自由 4 | 5 | # 效果图 6 | 7 | ![效果图](./picture/circlelayout.gif) 8 | 9 | # 用法 10 | 11 | ## 引入 12 | ```gradle 13 | compile 'com.nelson:CircleLayout:0.1.1' 14 | ``` 15 | ## 方法 16 | 1.可以直接在布局文件中进行布局,类似LinearLayout,但是这里不需要关心布局方式,会自动将布局中的所有子view均匀分布到中心点四周,这里你可以设置自己想要的半径 17 | ```xml 18 | 25 | 26 | 31 | 32 | 36 | 37 | ... 38 | 39 | 40 | 41 | ``` 42 | 2.也可以直接从代码中添加子View 43 | ```java 44 | CircleLayout circleLayout = (CircleLayout) findViewById(R.id.circle); 45 | circleLayout.addView(View v); 46 | ``` 47 | 3.还可以设置中心的View,这个功能目前只能在代码中设置: 48 | ```java 49 | circleLayout.setCenterView(View v); 50 | ``` 51 | 4.当然,这里还有`remove`和`get`方法,方便使用 52 | 5.如果想在代码中动态设置半径,可以直接调用 53 | ```java 54 | circleLayout.setRadius(int radius); 55 | ``` 56 | 6.可以控制该layout是否可滑动 57 | ```java 58 | circleLayout.setCanScroll(boolean isCanScroll); 59 | ``` 60 | 7.可以手动设置layout旋转角度(也可以在xml中设置) 61 | ```java 62 | circleLyoaut.setChangeCorner(double changeCorner) 63 | ``` 64 | 65 | 8.如果想给每一个子view添加动画啥的,可以直接对子view进行操作 66 | 67 | 9.更多用法请自行脑补 68 | 69 | 70 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | 4 | android { 5 | compileSdkVersion 25 6 | buildToolsVersion "25.0.2" 7 | defaultConfig { 8 | applicationId "com.nelson.circlelayoutdemo" 9 | minSdkVersion 19 10 | targetSdkVersion 25 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | compile fileTree(include: ['*.jar'], dir: 'libs') 25 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 26 | exclude group: 'com.android.support', module: 'support-annotations' 27 | }) 28 | compile 'com.android.support:appcompat-v7:25.3.1' 29 | compile 'com.android.support.constraint:constraint-layout:1.0.1' 30 | testCompile 'junit:junit:4.12' 31 | compile project(':CircleLayout') 32 | } 33 | -------------------------------------------------------------------------------- /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 D:\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/nelson/circlelayoutdemo/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.nelson.circlelayoutdemo; 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.nelson.circlelayoutdemo", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/nelson/circlelayoutdemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.nelson.circlelayoutdemo; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | import android.widget.Button; 7 | import android.widget.ImageView; 8 | import android.widget.SeekBar; 9 | import android.widget.Toast; 10 | 11 | import com.nelson.circlelayout.CircleLayout; 12 | 13 | public class MainActivity extends AppCompatActivity { 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_main); 19 | 20 | final CircleLayout circleLayout = (CircleLayout) findViewById(R.id.circle); 21 | ImageView imageView1 = (ImageView) findViewById(R.id.image1); 22 | Button button = (Button) findViewById(R.id.button); 23 | Button button2 = (Button) findViewById(R.id.button2); 24 | SeekBar seekBar = (SeekBar) findViewById(R.id.radium); 25 | 26 | circleLayout.setCanScroll(true); 27 | 28 | imageView1.setOnClickListener(new View.OnClickListener() { 29 | @Override 30 | public void onClick(View v) { 31 | Toast.makeText(MainActivity.this, "image1", Toast.LENGTH_SHORT).show(); 32 | } 33 | }); 34 | 35 | button.setOnClickListener(new View.OnClickListener() { 36 | @Override 37 | public void onClick(View v) { 38 | ImageView imageView = new ImageView(MainActivity.this); 39 | imageView.setImageDrawable(getResources().getDrawable(R.mipmap.ic_launcher_round)); 40 | circleLayout.setCenterView(imageView); 41 | } 42 | }); 43 | 44 | button2.setOnClickListener(new View.OnClickListener() { 45 | @Override 46 | public void onClick(View v) { 47 | circleLayout.removeCenterView(); 48 | } 49 | }); 50 | 51 | seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 52 | @Override 53 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 54 | circleLayout.setRadius(progress * 20); 55 | } 56 | 57 | @Override 58 | public void onStartTrackingTouch(SeekBar seekBar) { 59 | 60 | } 61 | 62 | @Override 63 | public void onStopTrackingTouch(SeekBar seekBar) { 64 | 65 | } 66 | }); 67 | 68 | 69 | 70 | 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelson1110/CircleLayout/a8ae04fb55c16686c77b789524d8631119d820b8/app/src/main/res/drawable/center.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelson1110/CircleLayout/a8ae04fb55c16686c77b789524d8631119d820b8/app/src/main/res/drawable/down.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nelson1110/CircleLayout/a8ae04fb55c16686c77b789524d8631119d820b8/app/src/main/res/drawable/up.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 18 | 19 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | 44 | 45 | 49 | 50 | 54 | 55 | 56 | 57 | 58 | 64 |