能有一個雙休的週末,對於程式設計師來說,也算是一件幸福的事情吧。苦逼的加了一週的班,終於可以休息放鬆放鬆了。作為一個LOL愛好者,週末最開心的事當然就是約上幾個小夥伴一起開黑了。一起超神、一起連跪,也算是週末的一大樂事。這幾天英雄聯盟搞活動,抽到一個安妮限定皮膚,可把我樂壞了,於是馬上就登陸掌盟客戶端檢視皮膚。進入皮膚瀏覽介面之後,覺得這個皮膚瀏覽的效果還真不錯,如下圖:
作為一個程式設計師,當然第一時間就是思考它是怎麼實現的?我能用什麼方法來實現類似的效果?於是花了半天的時間,做了一個類似的效果。因此本篇文章就分享一下如何實現這一效果。最後實現的效果如下:
思路與分析
在開始寫程式碼之前,我們還是來分析一下介面元素,和該用什麼技術來實現各個部分。
1,首先是整個介面的滑動,我們肯定一眼就能看出來,用ViewPager 實現。
2,ViewPager 滑動時有放大縮小的動畫,用ViewPager.Transfoemer 輕鬆搞定。
3,ViewPager 顯示多頁(展示前後頁面的部分)。
4,介面圖片的形狀,旋轉90度的等腰梯形。這個只能用自定義View來實現了。
5,整個介面的背景為當前顯示圖片的高斯模糊圖。
程式碼實現
上面分析了介面的構成元素,那麼現在我們就來看一下具體的實現。
1, ViewPager 展示多頁
這個問題在我們前一篇文章已經講過,這裡不再重複,就是用ViewGroup 的 clipChildren 屬性,值為false。也就是在整個佈局的跟節點新增下面一行程式碼:
android:clipChildren="false"複製程式碼
然後,ViewPager需要設定左右Margin,也就是前後頁顯示的位置
<android.support.v4.view.ViewPager
android:id="@+id/my_viewpager"
android:layout_width="wrap_content"
android:layout_height="300dp"
android:clipChildren="false"
android:layout_marginLeft="50dp"
android:layout_marginRight="50dp"
android:layout_centerInParent="true"
/>複製程式碼
從上面的效果圖可以看到,當前頁和前後頁的部分是有間距的,我們只需要在Item佈局中左右新增margin屬性:
android:layout_marginLeft="30dp"
android:layout_marginRight="30dp"複製程式碼
好了,這樣ViewPager就能顯示多頁,並且當前頁和前後頁之間還有一定的間距。
2, ViewPager 切換時的動畫
ViewPager 切換時的自定義動畫用ViewPager.PageTransformer
, 這個在上一篇文章也講過,沒看過的倒回去看一下。這裡不細講了,直接上程式碼:
public class CustomViewPagerTransformer implements ViewPager.PageTransformer {
private int maxTranslateOffsetX;
private ViewPager viewPager;
private static final float MIN_SCALE = 0.75f;
public CustomViewPagerTransformer(Context context) {
this.maxTranslateOffsetX = dp2px(context, 160);
}
public void transformPage(View view, float position) {
// position的可能性的值有,其實從官方示例的註釋就能看出:
//[-Infinity,-1) 已經看不到了
// (1,+Infinity] 已經看不到了
// [-1,1]
// 而我們從寫PageTransformer,操作View動畫的重點區間就在[-1,1]
if (viewPager == null) {
viewPager = (ViewPager) view.getParent();
}
int leftInScreen = view.getLeft() - viewPager.getScrollX();
int centerXInViewPager = leftInScreen + view.getMeasuredWidth() / 2;
int offsetX = centerXInViewPager - viewPager.getMeasuredWidth() / 2;
float offsetRate = (float) offsetX * 0.38f / viewPager.getMeasuredWidth();
float scaleFactor = 1 - Math.abs(offsetRate);
if (scaleFactor > 0) {
view.setScaleX(scaleFactor);
view.setScaleY(scaleFactor);
view.setTranslationX(-maxTranslateOffsetX * offsetRate);
}
}
/**
* dp和畫素轉換
*/
private int dp2px(Context context, float dipValue) {
float m = context.getResources().getDisplayMetrics().density;
return (int) (dipValue * m + 0.5f);
}
}複製程式碼
3, 自定義多邊形ImageView
多邊形ImageView,我們通過自定義的方式實現,繼承ImageView, 然後重寫onDraw()方法。這裡實現這種不規則的多邊形View有兩種方法。第一:使用PorterDuffXfermode,這種方法需要你給一個蒙板圖片,在onDraw 方法中,先繪製蒙板圖片,然後設定Paint的setXfermode
為PorterDuff.Mode.SRC_IN
,再繪製要顯示的圖片,這樣就能把顯示的圖片裁剪成蒙板的形狀。第二: 使用canvas的clipPath()
方法,我們用Path 來繪製多邊形,然後clipPath()
將畫布裁剪成繪製的形狀,然後在繪製要顯示的圖片。
關於PorterDuffXfermode 的更多用法,有興趣的可以去google 一下,網上有很多相關的文章。這裡我用的是兩種方法的結合,先用clipPath得到一個需要形狀的bitmap,然後使用PorterDuffXfermode。自定義View程式碼如下:
public class PolygonView extends AppCompatImageView {
private int mWidth = 0;
private int mHeight = 0;
private Paint mPaint;
private Paint mBorderPaint;
private PorterDuffXfermode mXfermode;
private Bitmap mBitmap;
private int mBorderWidth;
private Bitmap mMaskBitmap;
public PolygonView(Context context) {
super(context);
init();
}
public PolygonView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public PolygonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
mBorderWidth = DisplayUtils.dpToPx(4);
setLayerType(View.LAYER_TYPE_SOFTWARE, null);// 關閉硬體加速加速
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.RED);
mPaint.setDither(true);
mBorderPaint = new Paint();
mBorderPaint.setColor(Color.WHITE);
mBorderPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mBorderPaint.setAntiAlias(true);//抗鋸齒
mBorderPaint.setDither(true);//防抖動
mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
mMaskBitmap = getMaskBitmap();
}
@Override
public void setImageResource(@DrawableRes int resId) {
super.setImageResource(resId);
mBitmap = BitmapFactory.decodeResource(getResources(),resId);
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.drawBitmap(mMaskBitmap,0,0,mBorderPaint);
mPaint.setXfermode(mXfermode);
Bitmap bitmap = getCenterCropBitmap(mBitmap,mWidth,mHeight);
canvas.drawBitmap(bitmap,0,0,mPaint);
mPaint.setXfermode(null);
canvas.restore();
}
private Bitmap getMaskBitmap(){
Bitmap bm = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bm);
Point point1 = new Point(0,30);
Point point2 = new Point(mWidth,0);
Point point3 = new Point(mWidth,mHeight);
Point point4 = new Point(0,mHeight - 30);
Path path = new Path();
path.moveTo(point1.x,point1.y);
path.lineTo(point2.x,point2.y);
path.lineTo(point3.x,point3.y);
path.lineTo(point4.x,point4.y);
path.close();
c.drawPath(path,mBorderPaint);
return bm;
}
/**
* 對原圖進行等比裁剪
*/
private Bitmap scaleImage(Bitmap bitmap){
if(bitmap!=null){
int widht=bitmap.getWidth();
int height=bitmap.getHeight();
int new_width=0;
int new_height=0;
if(widht!=height){
if(widht>height){
new_height=mHeight;
new_width=widht*new_height/height;
}else{
new_width=mWidth;
new_height=height*new_width/widht;
}
}else{
new_width=mWidth;
new_height=mHeight;
}
return Bitmap.createScaledBitmap(bitmap, new_width, new_height, true);
}
return null;
}
private Bitmap getCenterCropBitmap(Bitmap src, float rectWidth, float rectHeight) {
float srcRatio = ((float) src.getWidth()) / src.getHeight();
float rectRadio = rectWidth / rectHeight;
if (srcRatio < rectRadio) {
return Bitmap.createScaledBitmap(src, (int)rectWidth, (int)((rectWidth / src.getWidth()) * src.getHeight()), false);
} else {
return Bitmap.createScaledBitmap(src, (int)((rectHeight / src.getHeight()) * src.getWidth()), (int)rectHeight, false);
}
}
}複製程式碼
建議:這裡使用clipPath方法的時候,會出現很多鋸齒,即使Paint 設定了抗鋸齒也沒啥用,所以建議使用PorterDuffXfermode 方法。要實現類似的效果,最好是找設計師要一張蒙板形狀圖。在用PorterDuffXfermode實現,簡單效果好。
通過上面的3步,其實整個 介面的效果差不多已經出來了,最後我們需要做的就是高斯模糊背景圖。
4, 背景圖高斯模糊
背景的高斯模糊就很簡單了,前面我也有寫過關於幾種高斯模糊方法的對比(Android 圖片高斯模糊解決方案),最後封裝了一個方便的庫(github.com/pinguo-zhou…),只需要簡單幾行程式碼就行。我們在ViewPager的onPageSelect方法中,獲取顯示的圖片,進行高斯模糊處理。
@Override
public void onPageSelected(int position) {
Bitmap source = BitmapFactory.decodeResource(getResources(),VPAdapter.RES[position]);
Bitmap bitmap = EasyBlur.with(getApplicationContext())
.bitmap(source)
.radius(20)
.blur();
mImageBg.setImageBitmap(bitmap);
mDesc.setText(mVPAdapter.getPageTitle(position));
}複製程式碼
最後,給出完整的佈局檔案和Activity程式碼:
1, activity佈局檔案:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:clipChildren="false"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 高斯模糊背景-->
<ImageView
android:id="@+id/activity_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
/>
<!-- Toolbar-->
<RelativeLayout
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="50dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/navigation_back_white"
android:layout_centerVertical="true"
android:layout_marginLeft="15dp"
/>
<TextView
android:id="@+id/title_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="18sp"
android:textColor="@android:color/white"
/>
</RelativeLayout>
<android.support.v4.view.ViewPager
android:id="@+id/my_viewpager"
android:layout_width="wrap_content"
android:layout_height="300dp"
android:clipChildren="false"
android:layout_marginLeft="50dp"
android:layout_marginRight="50dp"
android:layout_centerInParent="true"
/>
<com.zhouwei.indicatorview.CircleIndicatorView
android:id="@+id/indicatorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="60dp"
android:layout_centerHorizontal="true"
app:indicatorSelectColor="#C79EFE"
app:indicatorSpace="5dp"
app:indicatorRadius="8dp"
app:enableIndicatorSwitch="false"
app:indicatorTextColor="@android:color/white"
app:fill_mode="number"
app:indicatorColor="#C79EFE"
/>
<TextView
android:id="@+id/skin_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_below="@+id/my_viewpager"
android:layout_marginTop="20dp"
android:textColor="@android:color/white"
android:textSize="18sp"
/>
</RelativeLayout>複製程式碼
2, Activity程式碼:
public class ViewPagerActivity extends AppCompatActivity {
private ViewPager mViewPager;
private VPAdapter mVPAdapter;
private ImageView mImageBg;
private CircleIndicatorView mCircleIndicatorView;
private TextView mTitle,mDesc;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.viewpager_transform_layout);
View view = findViewById(R.id.toolbar);
StatusBarUtils.setTranslucentImageHeader(this, 0,view);
initView();
}
private void initView() {
mViewPager = (ViewPager) findViewById(R.id.my_viewpager);
mImageBg = (ImageView) findViewById(R.id.activity_bg);
mCircleIndicatorView = (CircleIndicatorView) findViewById(R.id.indicatorView);
mTitle = (TextView) findViewById(R.id.title_name);
mDesc = (TextView) findViewById(R.id.skin_desc);
mTitle.setText("黑暗之女");
mViewPager.setPageTransformer(false,new CustomViewPagerTransformer(this));
// 新增監聽器
mViewPager.addOnPageChangeListener(onPageChangeListener);
mVPAdapter = new VPAdapter(getSupportFragmentManager());
mViewPager.setAdapter(mVPAdapter);
mViewPager.setOffscreenPageLimit(3);
// Indicator 和ViewPager 建立關聯
mCircleIndicatorView.setUpWithViewPager(mViewPager);
// 首次進入展示第二頁
mViewPager.setCurrentItem(1);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return mViewPager.onTouchEvent(event);
}
private ViewPager.OnPageChangeListener onPageChangeListener = new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
Bitmap source = BitmapFactory.decodeResource(getResources(),VPAdapter.RES[position]);
Bitmap bitmap = EasyBlur.with(getApplicationContext())
.bitmap(source)
.radius(20)
.blur();
mImageBg.setImageBitmap(bitmap);
mDesc.setText(mVPAdapter.getPageTitle(position));
}
@Override
public void onPageScrollStateChanged(int state) {
}
};
}複製程式碼
ViewPager的每一個頁面用Fragment 來展示的,Fragment程式碼如下:
public class ItemFragment extends Fragment {
private PolygonView mPolygonView;
public static ItemFragment newInstance(int resId){
ItemFragment itemFragment = new ItemFragment();
Bundle bundle = new Bundle();
bundle.putInt("resId",resId);
itemFragment.setArguments(bundle);
return itemFragment;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.view_pager_muti_layout,null);
mPolygonView = (PolygonView) view.findViewById(R.id.item_image);
// 做一個屬性動畫
ObjectAnimator animator = ObjectAnimator.ofFloat(mPolygonView,"rotation",0f,10f);
animator.setDuration(10);
animator.start();
return view;
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
int resId = getArguments().getInt("resId");
mPolygonView.setImageResource(resId);// 設定圖片
}
}複製程式碼
說明:在Fragment中對PolygonView做了一個旋轉的動畫,是因為PolygonView 是一個豎著的等腰梯形,但是看效果圖,其實不是,還有一個小幅度的旋轉,如果將這個旋轉放在PolygonView 裡面做的話,發現每次ViewPager 切換的時候,都有一個旋轉動畫,效果不好,因此將動畫放在這裡。應該還有其他更優雅一點的方法,有興趣的可以去試一下。
最後
本篇文章是ViewPager 系列的第三篇文章,也是這個系列的最後一些文章,這三篇文章總結了ViewPager 的一些常用方法,如Banner 、切換動畫等等。還講了如何封裝一個擴充套件性強,比較通用的ViewPager。這也是對自己以前用過的這些知識點的一個總結和沉澱。喜歡的話可以關注我的簡書和掘金賬號,會不定期的更新Android相關的優質文章。如果有什麼問題的話也歡迎指出交流。Demo請訪問:github.com/pinguo-zhou…
ViewPager系列文章:
ViewPager系列之 打造一個通用的ViewPager
ViewPager系列之 仿魅族應用的廣告BannerView
如果你喜歡我的文章,歡迎關注我的微信公眾號:Android技術雜貨鋪,第一時間獲取有價值的Android乾貨文章。一起探討Android開發技術,一起成長。
微信公眾號:Android技術雜貨鋪