├── LICENSE ├── README.md └── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── release ├── app-release.apk └── output.json └── src ├── androidTest └── java │ └── com │ └── example │ └── zhangzhihao │ └── channelmanagedemo │ └── ExampleInstrumentedTest.java ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── example │ │ └── zhangzhihao │ │ └── channelmanagedemo │ │ ├── ChannelAdapter.java │ │ ├── ChannelBean.java │ │ ├── GridSpacingItemDecoration.java │ │ ├── ItemDragCallback.java │ │ └── MainActivity.java └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_launcher_background.xml │ └── icon_close.png │ ├── layout │ ├── activity_main.xml │ ├── adapter_channel.xml │ ├── adapter_more_channel.xml │ ├── adapter_tab.xml │ └── adapter_title.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.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 └── example └── zhangzhihao └── channelmanagedemo └── ExampleUnitTest.java /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 zzh12138 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChannelManageDemo_Java 2 | https://www.jianshu.com/p/57324eb516df 3 | ###### ChannelManageDemo by java 4 | ### 高仿腾讯新闻频道管理页面 5 | #### 用recyclerView+ItemTouchHelper实现 6 | ![avatar](https://raw.githubusercontent.com/zzh12138/ChannelManageDemo_Kotlin/master/app/src/main/assets/ezgif-5-d492977e87_kotlin.gif) 7 | 8 | ## License 9 | MIT 10 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 26 5 | defaultConfig { 6 | applicationId "com.example.zhangzhihao.channelmanagedemo" 7 | minSdkVersion 15 8 | targetSdkVersion 26 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation 'com.android.support:appcompat-v7:26.1.0' 24 | compile 'com.android.support:recyclerview-v7:26.1.0' 25 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 26 | testImplementation 'junit:junit:4.12' 27 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 28 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/release/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzh12138/ChannelManageDemo_Java/42eb374e29a8057e96b88e23b0ae04106d6369a5/app/release/app-release.apk -------------------------------------------------------------------------------- /app/release/output.json: -------------------------------------------------------------------------------- 1 | [{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":1},"path":"app-release.apk","properties":{"packageId":"com.example.zhangzhihao.channelmanagedemo","split":"","minSdkVersion":"15"}}] -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/zhangzhihao/channelmanagedemo/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.example.zhangzhihao.channelmanagedemo; 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 | * Instrumented 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.example.zhangzhihao.channelmanagedemo", 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/example/zhangzhihao/channelmanagedemo/ChannelAdapter.java: -------------------------------------------------------------------------------- 1 | package com.example.zhangzhihao.channelmanagedemo; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.AnimatorSet; 6 | import android.animation.ObjectAnimator; 7 | import android.content.Context; 8 | import android.graphics.Typeface; 9 | import android.support.v7.widget.RecyclerView; 10 | import android.text.Layout; 11 | import android.view.LayoutInflater; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.view.ViewTreeObserver; 15 | import android.widget.ImageView; 16 | import android.widget.LinearLayout; 17 | import android.widget.TextView; 18 | 19 | import java.util.Collections; 20 | import java.util.List; 21 | 22 | 23 | /** 24 | * Created by zhangzhihao on 2018/2/26. 25 | */ 26 | 27 | public class ChannelAdapter extends RecyclerView.Adapter { 28 | private Context mContext; 29 | private List mList; 30 | private List recommendList; //推荐频道 31 | private List cityList; //地方新闻 32 | private int selectedSize; 33 | private int fixSize; //已选频道中固定频道大小 34 | private boolean isRecommend; //当前是否显示推荐频道 35 | private onItemRangeChangeListener onItemRangeChangeListener; 36 | private int mLeft, mRight; //蓝色线条距离屏幕左边的距离 37 | private int mTabY; //Tab距离parent的Y的距离 38 | 39 | 40 | public ChannelAdapter(Context mContext, List mList, List recommendList, List cityList) { 41 | this.mContext = mContext; 42 | this.mList = mList; 43 | this.recommendList = recommendList; 44 | this.cityList = cityList; 45 | mLeft = -1; 46 | mRight = -1; 47 | } 48 | 49 | public void setOnItemRangeChangeListener(ChannelAdapter.onItemRangeChangeListener onItemRangeChangeListener) { 50 | this.onItemRangeChangeListener = onItemRangeChangeListener; 51 | } 52 | 53 | public int getSelectedSize() { 54 | return selectedSize; 55 | } 56 | 57 | public void setSelectedSize(int selectedSize) { 58 | this.selectedSize = selectedSize; 59 | } 60 | 61 | public int getFixSize() { 62 | return fixSize; 63 | } 64 | 65 | public void setFixSize(int fixSize) { 66 | this.fixSize = fixSize; 67 | } 68 | 69 | public void setRecommend(boolean recommend) { 70 | isRecommend = recommend; 71 | } 72 | 73 | @Override 74 | public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 75 | View view = LayoutInflater.from(mContext).inflate(viewType, parent, false); 76 | if (viewType == R.layout.adapter_channel) { 77 | return new ChannelHolder(view); 78 | } else if (viewType == R.layout.adapter_more_channel) { 79 | return new MoreChannelHolder(view); 80 | } else if (viewType == R.layout.adapter_tab) { 81 | return new TabHolder(view); 82 | } else { 83 | return new TitleHolder(view); 84 | } 85 | } 86 | 87 | @Override 88 | public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { 89 | if (holder instanceof ChannelHolder) { 90 | setChannel((ChannelHolder) holder, mList.get(position)); 91 | } else if (holder instanceof MoreChannelHolder) { 92 | setMoreChannel((MoreChannelHolder) holder); 93 | } else if (holder instanceof TabHolder) { 94 | setTab((TabHolder) holder); 95 | } else { 96 | 97 | } 98 | } 99 | 100 | private void setChannel(final ChannelHolder holder, ChannelBean bean) { 101 | final int position = holder.getLayoutPosition(); 102 | holder.name.setText(bean.getName()); 103 | holder.name.setOnClickListener(new View.OnClickListener() { 104 | @Override 105 | public void onClick(View v) { 106 | if (holder.getLayoutPosition() < selectedSize + 1) { 107 | //tab上面的 点击移除 108 | if (holder.getLayoutPosition() > fixSize) { 109 | removeFromSelected(holder); 110 | } 111 | } else { 112 | //tab下面的 点击添加到已选频道 113 | selectedSize++; 114 | itemMove(holder.getLayoutPosition(), selectedSize); 115 | notifyItemChanged(selectedSize); 116 | if (onItemRangeChangeListener != null) { 117 | onItemRangeChangeListener.refreshItemDecoration(); 118 | } 119 | } 120 | } 121 | }); 122 | holder.name.setOnLongClickListener(new View.OnLongClickListener() { 123 | @Override 124 | public boolean onLongClick(View v) { 125 | //返回true 防止长按拖拽事件跟点击事件冲突 126 | return true; 127 | } 128 | }); 129 | holder.delete.setOnClickListener(new View.OnClickListener() { 130 | @Override 131 | public void onClick(View v) { 132 | removeFromSelected(holder); 133 | } 134 | }); 135 | 136 | //tab下面的不显示删除按钮 137 | if (position - 1 < fixSize || position > selectedSize) { 138 | holder.delete.setVisibility(View.GONE); 139 | } else { 140 | holder.delete.setVisibility(View.VISIBLE); 141 | } 142 | } 143 | 144 | private void setMoreChannel(MoreChannelHolder holder) { 145 | holder.itemView.setOnClickListener(new View.OnClickListener() { 146 | @Override 147 | public void onClick(View v) { 148 | //todo to more channel activity 149 | } 150 | }); 151 | } 152 | 153 | private void setTab(final TabHolder holder) { 154 | final LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) holder.indicator.getLayoutParams(); 155 | //测量蓝色线条距离 156 | if (mLeft == -1) { 157 | holder.recommend.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 158 | @Override 159 | public boolean onPreDraw() { 160 | Layout layout = holder.recommend.getLayout(); 161 | mLeft = (int) (holder.recommend.getLeft() + layout.getPrimaryHorizontal(0)) - MainActivity.dip2px(mContext, 10); //textView左边距离+第一个文字绘制的距离 设置了padding 所以要减掉 162 | params.leftMargin = mLeft; 163 | holder.indicator.setLayoutParams(params); 164 | holder.recommend.getViewTreeObserver().removeOnPreDrawListener(this); 165 | return true; 166 | } 167 | }); 168 | } 169 | holder.city.setOnClickListener(new View.OnClickListener() { 170 | @Override 171 | public void onClick(View v) { 172 | if (isRecommend) { 173 | holder.city.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); 174 | holder.recommend.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL)); 175 | if (mRight == -1) { 176 | mRight = mLeft + holder.city.getLeft() - MainActivity.dip2px(mContext, 10); 177 | } 178 | params.leftMargin = mRight; 179 | isRecommend = false; 180 | recommendList.clear(); 181 | recommendList.addAll(mList.subList(selectedSize + 2, mList.size())); 182 | mList.removeAll(recommendList); 183 | mList.addAll(cityList); 184 | notifyDataSetChanged(); 185 | } 186 | } 187 | }); 188 | holder.recommend.setOnClickListener(new View.OnClickListener() { 189 | @Override 190 | public void onClick(View v) { 191 | if (!isRecommend) { 192 | holder.city.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL)); 193 | holder.recommend.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); 194 | params.leftMargin = mLeft; 195 | isRecommend = true; 196 | cityList.clear(); 197 | cityList.addAll(mList.subList(selectedSize + 2, mList.size())); 198 | mList.removeAll(cityList); 199 | mList.addAll(recommendList); 200 | notifyDataSetChanged(); 201 | } 202 | } 203 | }); 204 | final ViewTreeObserver observer = holder.itemView.getViewTreeObserver(); 205 | observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 206 | @Override 207 | public boolean onPreDraw() { 208 | //计算tab的Y,用于后面的动画 需要一直监听 209 | mTabY = holder.itemView.getTop(); 210 | return true; 211 | } 212 | }); 213 | } 214 | 215 | private void removeFromSelected(ChannelHolder holder) { 216 | int position = holder.getLayoutPosition(); 217 | holder.delete.setVisibility(View.GONE); 218 | ChannelBean bean = mList.get(position); 219 | if ((isRecommend && bean.isRecommend()) || (!isRecommend && !bean.isRecommend())) { 220 | //移除的频道属于当前tab显示的频道,直接调用系统的移除动画 221 | itemMove(position, selectedSize + 1); 222 | notifyItemRangeChanged(selectedSize + 1, 1); 223 | if (onItemRangeChangeListener != null) { 224 | //如果设置了itemDecoration,必须调用recyclerView.invalidateItemDecorations(),否则间距会不对 225 | onItemRangeChangeListener.refreshItemDecoration(); 226 | } 227 | } else { 228 | //不属于当前tab显示的频道 229 | removeAnimation(holder.itemView, isRecommend ? mRight : mLeft, mTabY, position); 230 | } 231 | selectedSize--; 232 | } 233 | 234 | private void removeAnimation(final View view, final float x, final float y, final int position) { 235 | final int fromX = view.getLeft(); 236 | final int fromY = view.getTop(); 237 | final ObjectAnimator animatorX = ObjectAnimator.ofFloat(view, "translationX", 0, x - fromX); 238 | final ObjectAnimator animatorY = ObjectAnimator.ofFloat(view, "translationY", 0, y - fromY); 239 | ObjectAnimator alpha = ObjectAnimator.ofFloat(view, "alpha", 1, 0); 240 | final AnimatorSet set = new AnimatorSet(); 241 | set.playTogether(animatorX, animatorY, alpha); 242 | set.setDuration(350); 243 | set.start(); 244 | set.addListener(new Animator.AnimatorListener() { 245 | @Override 246 | public void onAnimationStart(Animator animation) { 247 | 248 | } 249 | 250 | @Override 251 | public void onAnimationEnd(Animator animation) { 252 | if (isRecommend) { 253 | cityList.add(0, mList.get(position)); 254 | } else { 255 | recommendList.add(0, mList.get(position)); 256 | } 257 | mList.remove(position); 258 | notifyItemRemoved(position); 259 | onItemRangeChangeListener.refreshItemDecoration(); 260 | //这里需要重置view的属性 261 | resetView(view, x - fromX, y - fromY); 262 | } 263 | 264 | @Override 265 | public void onAnimationCancel(Animator animation) { 266 | 267 | } 268 | 269 | @Override 270 | public void onAnimationRepeat(Animator animation) { 271 | 272 | } 273 | }); 274 | } 275 | 276 | /** 277 | * 重置view的位置 278 | * 279 | * @param view 280 | * @param toX 281 | * @param toY 282 | */ 283 | private void resetView(View view, float toX, float toY) { 284 | ObjectAnimator animatorX = ObjectAnimator.ofFloat(view, "translationX", -toX, 0); 285 | ObjectAnimator animatorY = ObjectAnimator.ofFloat(view, "translationY", -toY, 0); 286 | ObjectAnimator alpha = ObjectAnimator.ofFloat(view, "alpha", 0, 1); 287 | AnimatorSet set = new AnimatorSet(); 288 | set.playTogether(animatorX, animatorY, alpha); 289 | set.setDuration(0); 290 | set.setStartDelay(5); 291 | set.start(); 292 | } 293 | 294 | void itemMove(int fromPosition, int toPosition) { 295 | if (fromPosition < toPosition) { 296 | for (int i = fromPosition; i < toPosition; i++) { 297 | Collections.swap(mList, i, i + 1); 298 | } 299 | } else { 300 | for (int i = fromPosition; i > toPosition; i--) { 301 | Collections.swap(mList, i, i - 1); 302 | } 303 | } 304 | notifyItemMoved(fromPosition, toPosition); 305 | } 306 | 307 | @Override 308 | public int getItemCount() { 309 | return mList == null ? 0 : mList.size(); 310 | } 311 | 312 | @Override 313 | public int getItemViewType(int position) { 314 | return mList.get(position).getLayoutId(); 315 | } 316 | 317 | class ChannelHolder extends RecyclerView.ViewHolder { 318 | TextView name; 319 | ImageView delete; 320 | 321 | public ChannelHolder(View itemView) { 322 | super(itemView); 323 | name = itemView.findViewById(R.id.channel_name); 324 | delete = itemView.findViewById(R.id.channel_delete); 325 | } 326 | } 327 | 328 | class TabHolder extends RecyclerView.ViewHolder { 329 | TextView recommend; 330 | TextView city; 331 | View indicator; 332 | 333 | public TabHolder(View itemView) { 334 | super(itemView); 335 | recommend = itemView.findViewById(R.id.recommend_channel); 336 | city = itemView.findViewById(R.id.city_channel); 337 | indicator = itemView.findViewById(R.id.indicator); 338 | } 339 | } 340 | 341 | class MoreChannelHolder extends RecyclerView.ViewHolder { 342 | 343 | public MoreChannelHolder(View itemView) { 344 | super(itemView); 345 | } 346 | } 347 | 348 | class TitleHolder extends RecyclerView.ViewHolder { 349 | 350 | public TitleHolder(View itemView) { 351 | super(itemView); 352 | } 353 | } 354 | 355 | interface onItemRangeChangeListener { 356 | void refreshItemDecoration(); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/zhangzhihao/channelmanagedemo/ChannelBean.java: -------------------------------------------------------------------------------- 1 | package com.example.zhangzhihao.channelmanagedemo; 2 | 3 | /** 4 | * Created by zhangzhihao on 2018/2/26. 5 | */ 6 | 7 | public class ChannelBean { 8 | private String name; 9 | private int spanSize; 10 | private int layoutId; 11 | private boolean isRecommend; 12 | 13 | public ChannelBean() { 14 | } 15 | 16 | public ChannelBean(String name, int spanSize, int layoutId, boolean isRecommend) { 17 | this.name = name; 18 | this.spanSize = spanSize; 19 | this.layoutId = layoutId; 20 | this.isRecommend = isRecommend; 21 | } 22 | 23 | public String getName() { 24 | return name; 25 | } 26 | 27 | public void setName(String name) { 28 | this.name = name; 29 | } 30 | 31 | public int getSpanSize() { 32 | return spanSize; 33 | } 34 | 35 | public void setSpanSize(int spanSize) { 36 | this.spanSize = spanSize; 37 | } 38 | 39 | public int getLayoutId() { 40 | return layoutId; 41 | } 42 | 43 | public void setLayoutId(int layoutId) { 44 | this.layoutId = layoutId; 45 | } 46 | 47 | public boolean isRecommend() { 48 | return isRecommend; 49 | } 50 | 51 | public void setRecommend(boolean recommend) { 52 | isRecommend = recommend; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/zhangzhihao/channelmanagedemo/GridSpacingItemDecoration.java: -------------------------------------------------------------------------------- 1 | package com.example.zhangzhihao.channelmanagedemo; 2 | 3 | import android.graphics.Rect; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.View; 6 | 7 | /** 8 | * Created by zhangzhihao on 2018/2/26. 9 | */ 10 | 11 | public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration { 12 | private int spanCount; 13 | private int spacing; 14 | private boolean includeEdge; 15 | private int tabPosition; 16 | 17 | public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge) { 18 | this.spanCount = spanCount; 19 | this.spacing = spacing; 20 | this.includeEdge = includeEdge; 21 | } 22 | 23 | @Override 24 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 25 | int position = parent.getChildAdapterPosition(view); 26 | if (position > 0) { 27 | int id = parent.getAdapter().getItemViewType(position); 28 | if (id == R.layout.adapter_tab) { 29 | tabPosition = position; 30 | } 31 | if (id == R.layout.adapter_channel) { 32 | if (position <= tabPosition) { 33 | position--; 34 | } else { 35 | position = position - (tabPosition + 1); 36 | } 37 | int column = position % spanCount; //列数 38 | if (includeEdge) { 39 | outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing) 40 | outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing) 41 | 42 | if (position < spanCount) { // top edge 43 | outRect.top = 20; 44 | } 45 | outRect.bottom = 20; // item bottom 46 | } else { 47 | outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing) 48 | outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing) 49 | if (position >= spanCount) { 50 | outRect.top = 20; // item top 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/zhangzhihao/channelmanagedemo/ItemDragCallback.java: -------------------------------------------------------------------------------- 1 | package com.example.zhangzhihao.channelmanagedemo; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Color; 5 | import android.graphics.DashPathEffect; 6 | import android.graphics.Paint; 7 | import android.graphics.PathEffect; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.support.v7.widget.helper.ItemTouchHelper; 10 | import android.view.View; 11 | 12 | import static android.support.v7.widget.helper.ItemTouchHelper.ACTION_STATE_DRAG; 13 | 14 | /** 15 | * Created by zhangzhihao on 2018/2/27. 16 | */ 17 | 18 | public class ItemDragCallback extends ItemTouchHelper.Callback { 19 | private static final String TAG = "ItemDragCallback"; 20 | private ChannelAdapter mAdapter; 21 | private Paint mPaint; //虚线画笔 22 | private int mPadding; //虚线框框跟按钮间的距离 23 | 24 | public ItemDragCallback(ChannelAdapter mAdapter, int mPadding) { 25 | this.mAdapter = mAdapter; 26 | this.mPadding = mPadding; 27 | mPaint = new Paint(); 28 | mPaint.setColor(Color.GRAY); 29 | mPaint.setAntiAlias(true); 30 | mPaint.setStrokeWidth(1); 31 | mPaint.setStyle(Paint.Style.STROKE); 32 | PathEffect pathEffect = new DashPathEffect(new float[]{5f, 5f}, 5f); //虚线 33 | mPaint.setPathEffect(pathEffect); 34 | } 35 | 36 | @Override 37 | public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { 38 | //固定位置及tab下面的channel不能拖动 39 | if (viewHolder.getLayoutPosition() < mAdapter.getFixSize() + 1 || viewHolder.getLayoutPosition() > mAdapter.getSelectedSize()) { 40 | return 0; 41 | } 42 | int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; 43 | int swipeFlags = 0; 44 | return makeMovementFlags(dragFlags, swipeFlags); 45 | } 46 | 47 | @Override 48 | public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { 49 | int fromPosition = viewHolder.getAdapterPosition(); //拖动的position 50 | int toPosition = target.getAdapterPosition(); //释放的position 51 | //固定位置及tab下面的channel不能拖动 52 | if (toPosition < mAdapter.getFixSize() + 1 || toPosition > mAdapter.getSelectedSize()) 53 | return false; 54 | mAdapter.itemMove(fromPosition, toPosition); 55 | return true; 56 | } 57 | 58 | @Override 59 | public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { 60 | 61 | } 62 | 63 | 64 | @Override 65 | public void onChildDrawOver(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { 66 | super.onChildDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); 67 | if (dX != 0 && dY != 0 || isCurrentlyActive) { 68 | //长按拖拽时底部绘制一个虚线矩形 69 | c.drawRect(viewHolder.itemView.getLeft(),viewHolder.itemView.getTop()-mPadding,viewHolder.itemView.getRight(),viewHolder.itemView.getBottom(),mPaint); 70 | } 71 | } 72 | 73 | @Override 74 | public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { 75 | super.onSelectedChanged(viewHolder, actionState); 76 | if(actionState==ACTION_STATE_DRAG){ 77 | //长按时调用 78 | ChannelAdapter.ChannelHolder holder= (ChannelAdapter.ChannelHolder) viewHolder; 79 | holder.name.setBackgroundColor(Color.parseColor("#FDFDFE")); 80 | holder.delete.setVisibility(View.GONE); 81 | holder.name.setElevation(5f); 82 | } 83 | } 84 | 85 | @Override 86 | public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { 87 | super.clearView(recyclerView, viewHolder); 88 | ChannelAdapter.ChannelHolder holder= (ChannelAdapter.ChannelHolder) viewHolder; 89 | holder.name.setBackgroundColor(Color.parseColor("#f0f0f0")); 90 | holder.name.setElevation(0f); 91 | holder.delete.setVisibility(View.VISIBLE); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/zhangzhihao/channelmanagedemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.zhangzhihao.channelmanagedemo; 2 | 3 | import android.content.Context; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.os.Bundle; 6 | import android.support.v7.widget.DefaultItemAnimator; 7 | import android.support.v7.widget.GridLayoutManager; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.support.v7.widget.helper.ItemTouchHelper; 10 | import android.view.WindowManager; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | public class MainActivity extends AppCompatActivity implements ChannelAdapter.onItemRangeChangeListener { 16 | 17 | private RecyclerView mRecyclerView; 18 | private List mList; 19 | private ChannelAdapter mAdapter; 20 | private String select[] = {"要闻", "体育", "新时代", "汽车", "时尚", "国际", "电影", "财经", "游戏", "科技", "房产", "政务", "图片", "独家"}; 21 | private String recommend[] = {"娱乐", "军事", "文化", "视频", "股票", "动漫", "理财", "电竞", "数码", "星座", "教育", "美容", "旅游"}; 22 | private String city[] = {"重庆", "深圳", "汕头", "东莞", "佛山", "江门", "湛江", "惠州", "中山", "揭阳", "韶关", "茂名", "肇庆", "梅州", "汕尾", "河源", "云浮", "四川"}; 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_main); 28 | mRecyclerView = findViewById(R.id.recyclerView); 29 | mList = new ArrayList<>(); 30 | GridLayoutManager manager = new GridLayoutManager(this, 4); 31 | manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { 32 | @Override 33 | public int getSpanSize(int position) { 34 | return mList.get(position).getSpanSize(); 35 | } 36 | }); 37 | mRecyclerView.setLayoutManager(manager); 38 | DefaultItemAnimator animator = new DefaultItemAnimator(); 39 | animator.setMoveDuration(300); //设置动画时间 40 | animator.setRemoveDuration(0); 41 | mRecyclerView.setItemAnimator(animator); 42 | ChannelBean title = new ChannelBean(); 43 | title.setLayoutId(R.layout.adapter_title); 44 | title.setSpanSize(4); 45 | mList.add(title); 46 | for (String bean : select) { 47 | mList.add(new ChannelBean(bean, 1, R.layout.adapter_channel, true)); 48 | } 49 | ChannelBean tabBean = new ChannelBean(); 50 | tabBean.setLayoutId(R.layout.adapter_tab); 51 | tabBean.setSpanSize(4); 52 | mList.add(tabBean); 53 | List recommendList = new ArrayList<>(); 54 | for (String bean : recommend) { 55 | recommendList.add(new ChannelBean(bean, 1, R.layout.adapter_channel, true)); 56 | } 57 | List cityList = new ArrayList<>(); 58 | for (String bean : city) { 59 | cityList.add(new ChannelBean(bean, 1, R.layout.adapter_channel, false)); 60 | } 61 | ChannelBean moreBean = new ChannelBean(); 62 | moreBean.setLayoutId(R.layout.adapter_more_channel); 63 | moreBean.setSpanSize(4); 64 | cityList.add(moreBean); 65 | mList.addAll(recommendList); 66 | mAdapter = new ChannelAdapter(this, mList, recommendList, cityList); 67 | mAdapter.setFixSize(1); 68 | mAdapter.setSelectedSize(select.length); 69 | mAdapter.setRecommend(true); 70 | mAdapter.setOnItemRangeChangeListener(this); 71 | mRecyclerView.setAdapter(mAdapter); 72 | WindowManager m = (WindowManager) getSystemService(Context.WINDOW_SERVICE); 73 | int spacing = (m.getDefaultDisplay().getWidth() - dip2px(this, 70) * 4) / 5; 74 | mRecyclerView.addItemDecoration(new GridSpacingItemDecoration(4,spacing,true)); 75 | ItemDragCallback callback=new ItemDragCallback(mAdapter,2); 76 | ItemTouchHelper helper=new ItemTouchHelper(callback); 77 | helper.attachToRecyclerView(mRecyclerView); 78 | } 79 | 80 | public static int dip2px(Context context, float dpValue) { 81 | float scale = context.getResources().getDisplayMetrics().density; 82 | return (int) (dpValue * scale + 0.5f); 83 | } 84 | 85 | @Override 86 | public void refreshItemDecoration() { 87 | mRecyclerView.invalidateItemDecorations(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzh12138/ChannelManageDemo_Java/42eb374e29a8057e96b88e23b0ae04106d6369a5/app/src/main/res/drawable/icon_close.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/adapter_channel.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 17 | 18 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/adapter_more_channel.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/adapter_tab.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 24 | 25 | 32 | 33 | 34 | 39 | 40 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/layout/adapter_title.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzh12138/ChannelManageDemo_Java/42eb374e29a8057e96b88e23b0ae04106d6369a5/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzh12138/ChannelManageDemo_Java/42eb374e29a8057e96b88e23b0ae04106d6369a5/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzh12138/ChannelManageDemo_Java/42eb374e29a8057e96b88e23b0ae04106d6369a5/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzh12138/ChannelManageDemo_Java/42eb374e29a8057e96b88e23b0ae04106d6369a5/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzh12138/ChannelManageDemo_Java/42eb374e29a8057e96b88e23b0ae04106d6369a5/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzh12138/ChannelManageDemo_Java/42eb374e29a8057e96b88e23b0ae04106d6369a5/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzh12138/ChannelManageDemo_Java/42eb374e29a8057e96b88e23b0ae04106d6369a5/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzh12138/ChannelManageDemo_Java/42eb374e29a8057e96b88e23b0ae04106d6369a5/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzh12138/ChannelManageDemo_Java/42eb374e29a8057e96b88e23b0ae04106d6369a5/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzh12138/ChannelManageDemo_Java/42eb374e29a8057e96b88e23b0ae04106d6369a5/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ChannelManageDemo 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/example/zhangzhihao/channelmanagedemo/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.example.zhangzhihao.channelmanagedemo; 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 | } --------------------------------------------------------------------------------