show code block

2017年11月28日 星期二

Android元件(TabLayout + ViewPager)– FragmentPagerAdapter簡易使用(二) 、TabLayout指示器

前言

除了ViewPager頁面的轉換,現在上面大多都還會有一個Tab
這邊使用我之前的DEMO 直接加上TabLayout
有兩篇可以先參考一下
 FragmentPagerAdapter簡易使用(一)
http://nikeru8.blogspot.tw/2017/11/androidfragmentpageradapterviewpager.html
還有
簡單ViewPager頁面 banner自動擴充實現
http://nikeru8.blogspot.tw/2017/11/androidviewpager-viewpager-banner.html

上面兩個demo都可以直接配合這次的TabLayout完成!
 如圖

今日實作


程式碼

First –

看圖說故事。
然後打上design
看到他後把他加入Gradle內,就開始使用囉




畫面xml拉好後
<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.hello.kaiser.fragment.MainActivity">

    <android.support.design.widget.TabLayout
        android:id="@+id/view_tab"
        android:layout_width="match_parent"
        android:layout_height="44dp" />

    <android.support.v4.view.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/view_tab"
        android:background="@android:color/darker_gray" />

</RelativeLayout>
 



設定一下元件 findViewById
    private TabLayout mTab;
    mTab = (TabLayout) findViewById(R.id.view_tab);
 

讓我們來加入Tab吧
 mTab.addTab(mTab.newTab().setText("第一頁"));
 mTab.addTab(mTab.newTab().setText("第二頁"));
 

這就可以在上方產生兩個Tab

然後要讓ViewPager滑動時,上方Tab跟著滑動
 mViewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(mTab));
 

除了滑動之外
點擊Tab,下方的ViewPager當然也要有所動作
mTab.addOnTabSelectedListener(new TabLayout.ViewPagerOnTabSelectedListener(mViewPager));
 

最簡單的使用方式就完成囉

如果你還想要再按下Tab時候,有額外的事件發生

可以設定她的監聽Listener
mTab.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
            }

            @Override
            public void onTabUnselected(TabLayout.Tab tab) {

            }

            @Override
            public void onTabReselected(TabLayout.Tab tab) {

            }
        });
 

簡單的方式大致上就這樣

這邊再講一個新增Tab的方法

可以在xml直接上TabItem


<android.support.design.widget.TabLayout
        android:id="@+id/view_tab"
        android:layout_width="match_parent"
        android:layout_height="44dp">

        <android.support.design.widget.TabItem
            android:id="@+id/one"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="one" />

        <android.support.design.widget.TabItem
            android:id="@+id/two"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="two" />

    </android.support.design.widget.TabLayout>
 
和ViewPager連動的部分運用上面的code在做連動

app:tabIndicatorColor指示條的顏色
app:tabIndicatorHeight指示條的高度
app:tabSelectedTextColor tab被選中時的字體顏色
app:tabTextColor tab未被選中時的字體顏色
app:tabMode="scrollable" : 默認是fixed:固定的,標籤很多時候會被擠壓,不能滑動。


 增加Tab的部分,這邊介紹了兩種,個人覺得使用代碼的方式呈現比較好,因為後台如果要新增Tab,這樣又要再xml上新增TabItem
如果使用代碼呈現,可以用for迴圈讓Tab的數量隨著後台的要求成長!



可能發生的問題

在普通的手機上都很正常,如果你在平板上造正常步驟設置妳的TabLayout
會有跑版問題,字體會往中間集中,下方那條指示器indicator也會變得很短。

在我百思不得其解下Rick大神解決了我的困難,在此歌頌一下師傅。

xml內設置
  <android.support.design.widget.TabLayout
       ...
        app:tabGravity="fill"
        app:tabMaxWidth="0dp"
       ...
        />
 
建議在使用TabLayout時都把這兩個加上,在平板上就不會跑版了。
感恩Seafood!讚嘆Seafood!



DEMO
https://github.com/nikeru8/FragmentPagerAdapterDemo/tree/addTabLayout

文獻:
http://www.jianshu.com/p/be1e8a1da639
http://givemepass-blog.logdown.com/posts/288943-how-to-use-tablayout

2017年11月27日 星期一

Android元件(FragmentPagerAdapter、ViewPager)– FragmentPagerAdapter簡易使用(一)

前言

之前寫了一個簡單的ViewPager範本。

簡單ViewPager頁面 banner自動擴充實現 
 http://nikeru8.blogspot.tw/2017/11/androidviewpager-viewpager-banner.html

 這篇偏向Banner

現在寫的FragmentPagerAdapter偏向頁面的使用方式


如下圖





重點程式碼

 step one FragmentPagerAdapter建構
先來看Adapter的部分 FragmentPagerAdapter

需要有幾個方法要複寫
 //必要建構子
    public adapter(FragmentManager fm) {
        super(fm);
    }

    //你想要顯示的Fragment
    @Override
    public Fragment getItem(int position) {
        return null;
    }

    //要顯示的筆數
    @Override
    public int getCount() {
        return 0;
    }
 

step two – Fragment

再來就是Fragment的部分
在開始前,先來看一下Fragment的生命週期

在這邊我們可以複寫onCreateonCreateView

並創建newInstance

newInstance可以自動創建
public static FragmentSingle newInstance() {
        //這邊設計呼叫Fragment的方式,Bundle內可以從activity > adapter 帶參數過來
        Bundle args = new Bundle();
        FragmentSingle fragment = new FragmentSingle();
        fragment.setArguments(args);
        return fragment;
    }

 
newInstance使用Bundle接參數帶入fragment
onCreate的地方真正的把參數帶到fragment

 //繼承fragment後,直接打newInstance會自動生成此方法,裡面都幫你建置好Bundle類了
    public static CreateFragment newInstance(int showType, String content) {
        //Bundle接收傳過來的參數
        Bundle args = new Bundle();
        args.putInt("showType", showType);
        args.putString("content", content);
        CreateFragment fragment = new CreateFragment();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //帶入參數
        showType = getArguments().getInt("showType");
        content = getArguments().getString("content");
    }
 

接下來還要複寫兩種方法
onCreateViewonViewCreated

onCreateView:用來創建Layout xml的,你想創建什麼型態的xml也可以設定在這裡,有在上方圖片的生命週期內

onViewCreated:用來取得參數,理所當然的這個方法就會在onCreateView之後,使用getView()這個方法取得xml內的id

這邊舉個onViewCreated的使用
 
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    TextView message = getView().findViewById(R.id.item_one);
    message.setText(content);
}


大致上這樣。

這邊說明一下比較妥當的使用時機。

我是希望程式碼已不攏長,可讀性為主,所以可以的話,讓頁面長的雷同的地方再使用會比較好。

舉個例子:



像是這個App這頁面,有著高相似性的頁面,使用這方法就會是上上之選。

STEP THREE –

Fragment寫好後可以把它塞入Adapter內了

public class CustomFragmentAdapter extends FragmentPagerAdapter {

    String mTitleOne, mTitleTwo;

    public CustomFragmentAdapter(FragmentManager fm) {
        super(fm);
    }

    public void setDataOne(String messgae) {
        //這邊你可以在activity內獲取資料,再利用getItem傳入CreateFragment
        mTitleOne = messgae;
        notifyDataSetChanged();
    }

    public void setDataTwo(String message) {
        //這邊你可以在activity內獲取資料,再利用getItem傳入CreateFragment
        mTitleTwo = message;
        notifyDataSetChanged();
    }

    @Override
    public Fragment getItem(int position) {
        //創作每一個Fragment內的內容
        if (position == 0) {//第一頁
            return new CreateFragment().newInstance(position, mTitleOne);
        } else {//第二頁
            return new CreateFragment().newInstance(position, mTitleTwo);
        }
    }

    @Override
    public int getCount() {
        //你得Fragment頁面數
        return 2;
    }
}
 

說明我寫在方法的上方。
此時你會看到我多寫了兩個方法setDataOnesetDataTwo
如果你兩個頁面的內容相差很多,要帶入不同的參數進去,可以使用這個方法和Activity要資料,在getItemnew不同的Fragment把資料帶進去。

當然你也可以直接寫在Adapter建構子內直接取值,但如果之後需要刷新fragment內的資料就要重新new一個Adapter,不是很聰明的作法。


完整程式碼

今日會動到的地方



簡單一個ViewPager
activity_main.xml
activity_main.xml
<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.hello.kaiser.fragment.MainActivity">

    <android.support.v4.view.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/darker_gray" />

</RelativeLayout>
 
create_fragment_one.xml
create_fragment_one.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:background="@color/colorPrimary"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/item_one"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="one"
        android:textSize="40sp" />
</LinearLayout>
 

create_fragment_two.xml
create_fragment_two.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorAccent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/item_two"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="two"
        android:textSize="40sp" />
</LinearLayout>
 

畫面畫好了 來看程式碼吧

CustomFragmentAdapter.java
public class CustomFragmentAdapter extends FragmentPagerAdapter {

    String mTitleOne, mTitleTwo;

    public CustomFragmentAdapter(FragmentManager fm) {
        super(fm);
    }

    public void setDataOne(String messgae) {
        //這邊你可以在activity內獲取資料,再利用getItem傳入CreateFragment
        mTitleOne = messgae;
        notifyDataSetChanged();
    }

    public void setDataTwo(String message) {
        //這邊你可以在activity內獲取資料,再利用getItem傳入CreateFragment
        mTitleTwo = message;
        notifyDataSetChanged();
    }

    @Override
    public Fragment getItem(int position) {
        //創作每一個Fragment內的內容
        if (position == 0) {//第一頁
            return new CreateFragment().newInstance(position, mTitleOne);
        } else {//第二頁
            return new CreateFragment().newInstance(position, mTitleTwo);
        }
    }

    @Override
    public int getCount() {
        //你得Fragment頁面數
        return 2;
    }
}

 

CreateFragment.java
public class CreateFragment extends Fragment {

    private int showType;
    private String content;
    private Context context;

    //繼承fragment後,直接打newInstance會自動生成此方法,裡面都幫你建置好Bundle類了
    public static CreateFragment newInstance(int showType, String content) {
        //Bundle接收傳過來的參數
        Bundle args = new Bundle();
        args.putInt("showType", showType);
        args.putString("content", content);
        CreateFragment fragment = new CreateFragment();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //帶入參數
        showType = getArguments().getInt("showType");
        content = getArguments().getString("content");
    }


    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        Log.d("checkpoint", "onCreateView onCreateView");
        if ("0".equals(String.valueOf(showType))) {//第一頁
            return LayoutInflater.from(context).inflate(R.layout.create_fragment_one, null);
        } else if ("1".equals(String.valueOf(showType))) {//第二頁
            return LayoutInflater.from(context).inflate(R.layout.create_fragment_two, null);
        } else {
            return null;
        }
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        Log.d("checkpoint", "checkonViewCreatedpoint onViewCreated");

        super.onViewCreated(view, savedInstanceState);
        if (showType == 0) {//第一頁
            TextView message = getView().findViewById(R.id.item_one);
            message.setText(content);

        } else if (showType == 1) {//第二頁
            TextView message = getView().findViewById(R.id.item_two);
            message.setText(content);
        }
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        this.context = context;
    }
}

 

MainActivity.java
public class MainActivity extends AppCompatActivity {

    private ViewPager mViewPager;
    private CustomFragmentAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initSet();
    }

    private void initView() {
        mViewPager = (ViewPager) findViewById(R.id.view_pager);
    }

    private void initSet() {
        mAdapter = new CustomFragmentAdapter(getSupportFragmentManager());
        mViewPager.setAdapter(mAdapter);
        mAdapter.setDataOne("你好第一頁");
        mAdapter.setDataTwo("歡迎光靈第二頁 hello ");
    }
}

 


DEMO
https://github.com/nikeru8/FragmentPagerAdapterDemo/tree/master



歡迎交流喔!



Fragment全集
 

Fragment的使用() ─ activity內放置Fragment
Fragment的使用() ─ activity內切換Fragment

Fragment 返回上一頁 OnBackPressed
Fragment點擊穿透
FragmentPagerAdapter

2017年11月26日 星期日

Android元件(AsyncTask)— 非同步任務、異步任務

前言

現在下載東西大多都使用第三方套件,異步任務都已經幫你處理得妥妥的。
但如果你要生成一張很大的圖片或是很好資源的任何東西,你就會需要它

這邊貼個介紹
http://aiur3908.blogspot.tw/2015/06/android-asynctask.html
超好懂的AsyncTask

以下簡略轉貼
求詳細請點上面網址

今日實作


重點程式碼

AsyncTask<Params, Progress, Result>



你開的class 會需要繼承 AsyncTask

而後面的三個參數

Params  代表著你要放入的參數,像是String 或是 int 

Progress 如果你要下載東西,這邊專門給你用進度條的參數

Result 就是你希望這條你新分出去的執行緒要回到主執行緒的時候要返回什麼東西。
如果你是拿來下載圖片,就可以返回Bitma。如果你是跑一個陣列,也可以返回ArrayList<>之類的....


而繼承AsyncTask會產生四個方法

onPreExecute  執行前,一些基本設定可以在這邊做。

doInBackground  執行中,在背景做任務。這邊就是重點中的重點,你希望下載或做事情的方法寫在這邊

onProgressUpdate  執行中,專門給Porgress條做監聽的方法

onPostExecute  執行後,最後的結果會在這邊。如果你Result的部分設定返回圖片,這裡就會噴Bitmap給你

方法會長以下這樣 

private class GetImage extends AsyncTask<String , Integer , Bitmap>{

        @Override
        protected void onPreExecute() {
            //執行前 設定可以在這邊設定
            super.onPreExecute();
        }

        @Override
        protected Bitmap doInBackground(String... params) {
            //執行中 在背景做事情
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            //執行中 可以在這邊告知使用者進度
            super.onProgressUpdate(values);
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            //執行後 完成背景任務
            super.onPostExecute(bitmap);
        }
}
 

String... 和 int... 的部分可以參考我寫的一篇猛虎出閘
http://nikeru8.blogspot.tw/2017/08/java.html

如果是複數的Params,使用方式就是Params[0]、Params[1]依此類推...

Params[0] 就會是helloOne
Params[1] 就是 helloTwo

使用範例

private class GetImage extends AsyncTask<String , Integer , Bitmap>{

        private ProgressDialog progressBar;
        //進度條元件

        @Override
        protected void onPreExecute() {
            //執行前 設定可以在這邊設定
            super.onPreExecute();

            progressBar = new ProgressDialog(MainActivity.this);
            progressBar.setMessage("Loading...");
            progressBar.setCancelable(false);
            progressBar.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
            progressBar.show();
            //初始化進度條並設定樣式及顯示的資訊。
        }

        @Override
        protected Bitmap doInBackground(String... params) {
            //執行中 在背景做事情
            int progress = 0;
            for (String urlStr : params) {
                try {
                    URL url = new URL(urlStr);
                    Bitmap bitmap = BitmapFactory.decodeStream(url.openConnection().getInputStream());
                } catch (Exception  e) {
                    e.printStackTrace();
                }
                publishProgress(progress+=33);
                //有三張圖 每張圖33%
            }
            publishProgress(100);
            //最後達到100%
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            //執行中 可以在這邊告知使用者進度
            super.onProgressUpdate(values);
            progressBar.setProgress(values[0]);
            //取得更新的進度
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            //執行後 完成背景任務
            super.onPostExecute(bitmap);

            progressBar.dismiss();
            //當完成的時候,把進度條消失
            imageView.setImageBitmap(bitmap);
        }
    }
 


完整程式碼

main_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:clickable="true"
        android:gravity="center"
        android:onClick="onbuttonclick"
        android:text="產生QRCode" />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/button"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="45dp" />
</RelativeLayout>
 

MainActivity.java
public class MainActivity extends AppCompatActivity {


    private Activity activity;

    private ProgressDialog loadingDialog;

    //要生成的QRCode內容
    String QRCodeContent = "http://nikeru8.blogspot.tw/2017/04/third-partyxzingcore-qrcodejar.html";
    ImageView imageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        activity = this;
        //參數
        imageView = (ImageView) findViewById(R.id.imageView);

        //設定loading Dialog
        loadingDialog = new ProgressDialog(activity);
        loadingDialog.setCanceledOnTouchOutside(false);
        loadingDialog.setCancelable(true);
        loadingDialog.setMessage("loading please wait...");
    }

    public void onbuttonclick(View view) {
        new GetImage().execute(QRCodeContent);
    }


    private class GetImage extends AsyncTask<String, Integer, Bitmap> {

        @Override
        protected void onPreExecute() {
            //執行前 設定可以在這邊設定

            loadingDialog.show();
            super.onPreExecute();
        }

        @Override
        protected Bitmap doInBackground(String... params) {
            //執行中 在背景做事情
            int QRCodeWidth = 600;
            int QRCodeHeight = 600;
            //QRCode內容編碼
            Map hints = new EnumMap(EncodeHintType.class);
            hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");

            MultiFormatWriter writer = new MultiFormatWriter();
            try {
                //ErrorCorrectionLevel容錯率分四級:L(7%) M(15%) Q(25%) H(30%)
                hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);

                //建立QRCode的資料矩陣
                BitMatrix result = writer.encode(params[0], BarcodeFormat.QR_CODE, QRCodeWidth, QRCodeHeight, hints);

                //建立矩陣圖
                Bitmap bitmap = Bitmap.createBitmap(QRCodeWidth, QRCodeHeight, Bitmap.Config.ARGB_4444);
                for (int y = 0; y < QRCodeHeight; y++) {
                    for (int x = 0; x < QRCodeWidth; x++) {
                        bitmap.setPixel(x, y, result.get(x, y) ? Color.BLACK : Color.WHITE);
                    }
                }

                return bitmap;

            } catch (WriterException e) {
                e.printStackTrace();
                return null;
            }

        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            //執行中 可以在這邊告知使用者進度
            super.onProgressUpdate(values);
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            //執行後 完成背景任務
            super.onPostExecute(bitmap);
            imageView.setImageBitmap(bitmap);
            loadingDialog.dismiss();
        }
    }
}

 


Demo


我拿QRCode寫個範例

因為QRCode如果跑太大張,通常都會造成畫面卡頓

使用AsyncTask就可以完美解決這問題

https://github.com/nikeru8/QRCodeDemo

2017年11月22日 星期三

Android元件(Indicator) – 指示器Indicator,ViewPager下方的圓點

前言


目前市面上已經很多Banner的第三方可以使用了
甚至是已經套好的ViewPager第三方 和Banner第三方。



這邊提供一個我自己常用的Indicator第三方
https://github.com/romandanylyk/PageIndicatorView

可以下載我的ViewPager demo 自己套套看這個第三方

情況是這樣的:
前幾天我使用Fragment,發現如果同時創建兩個相同的Fragment但不同內容的頁面,這個套件的Indicator會指向同一個id

 簡而言之就是兩個Banner會共用到同一個Indicator


只好自己寫一個Indicator來用
ViewPager Banner 內的指示器




本日實作


先下載ViewPagerDemo我直接在這篇加內容


首先呢 先下載制作Indicator的圖片
分別是
圖片一

圖片二

有看到東西嗎?

沒看到沒關係,因為我也沒看到XD  本來就是白色的

在這講一下,一個是空心圓 一個是實心圓

下載的地方幫你擺在這 點我下載


改動的部分
綠色為新增的

一、先把你剛剛下載的圖片放到drawable內

此時你會注意看到圖片內有一個xml檔案叫indicator_selector.xml


這其實可以自己創

每個item後面就是這張圖片被按到後的狀態
state_selected是被選取的狀態,true 是被選到,false則相反
看上圖,可以看出當true(被選取時),是實心圓,false(沒被選取)則是空心圓

這種寫法同樣可以套用在按鈕上,使用TextView設計一個按鈕,按下時的和非按下時會顯示不一樣的圖。詳情見:http://nikeru8.blogspot.tw/2017/03/textview.html


二、創建attrs.xml



命名為 attrs

attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="indicatorIcon" format="reference" />
    <attr name="indicatorMargin" format="dimension" />
    <attr name="indicatorSmooth" format="boolean" />
    <attr name="indicatorSize" format="dimension" />

    <declare-styleable name="IndicatorView">
        <!--指示器樣式-->
        <attr name="indicatorIcon" />
        <!--指示器的間距-->
        <attr name="indicatorMargin" />
        <!--指示器是否顯示滑動效果-->
        <attr name="indicatorSmooth" />
        <!--指示器的大小-->
        <attr name="indicatorSize" />
    </declare-styleable>

</resources> 

這裡是幹嘛的呢?
讓我們把畫面轉到activity_main.xml稍微更改了一下xml

activity_main.xml
<?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"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="250dp">

        <android.support.v4.view.ViewPager
            android:id="@+id/view_pager"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />


        <com.hello.kaiser.viewpager.IndicatorView
            android:id="@+id/id_view_indicator"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            android:layout_gravity="center_horizontal|bottom"
            android:layout_marginBottom="32dp"
            app:indicatorSmooth="false"
            custom:indicatorIcon="@drawable/indicator_selector"
            custom:indicatorMargin="5dp"
            custom:indicatorSize="9dp" />
    </RelativeLayout>
</RelativeLayout>
 

此時你會發現 在最外層的RelativeLayout 內
多了一個  xmlns:custom="http://schemas.android.com/apk/res-auto"
然後再看看新多的IndicatorView
是不是有剛剛在attrs內新增的參數了,indicatorIconindicatorMargin ...這類的

那要如何使用它呢?

創一個IndicatorView.java
public class IndicatorView extends View implements ViewPager.OnPageChangeListener{

    private Drawable mIndicator; //指示器的显示样式
    private int mIndicatorSize; //指示器的大小
    private boolean mSmooth; //是否显示滑动效果
    private int mMargin;  //指示器之间的间距

    private int mDefaultMargin; //默认的间距
    private int mSelectedItem; //选中的指示器

    private int mWidth; //测量后得到的控件的宽度
    private int mCount; //viewpager的页数,即指示器的个数
    private int mContextWidth; //实际计算得到的控件的宽度
    private float mOffset; //滑动偏移量


    private ViewPager.OnPageChangeListener mPageChangeListener;

    public IndicatorView(Context context) {
        this(context,null);
    }

    public IndicatorView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public IndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context,attrs,defStyleAttr);
    }

    private void init(Context context, AttributeSet attrs, int defStyleAttr) {

        //设置指示器大小的默认值
        mIndicatorSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,16,getResources().getDisplayMetrics());
        //设置指示器间距的默认值
        mDefaultMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,5,getResources().getDisplayMetrics());

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IndicatorView,defStyleAttr,0);

        mIndicator = typedArray.getDrawable(R.styleable.IndicatorView_indicatorIcon);
        mMargin = (int) typedArray.getDimension(R.styleable.IndicatorView_indicatorMargin,mDefaultMargin);
        mSmooth = typedArray.getBoolean(R.styleable.IndicatorView_indicatorSmooth,true);
        mIndicatorSize = (int) typedArray.getDimension(R.styleable.IndicatorView_indicatorSize,mIndicatorSize);

        typedArray.recycle();

        mIndicator.setBounds(0,0,mIndicatorSize,mIndicatorSize);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec),measureHeight(heightMeasureSpec));
    }

    /**
     * 测量宽
     * @param widthMeasureSpec
     * @return
     */
    private int measureWidth(int widthMeasureSpec) {
        final int mode = MeasureSpec.getMode(widthMeasureSpec);
        final int size = MeasureSpec.getSize(widthMeasureSpec);

        int width;
        //计算整个控件的宽度,其中 mCount 是获取到的ViewPager的页数
        int desired = getPaddingLeft() + mIndicatorSize * mCount + mMargin * (mCount - 1) + getPaddingRight();
        mContextWidth = desired;

        if (mode == MeasureSpec.EXACTLY) {
            //如果是match_parent,则选取屏幕宽度和计算得到的宽度中较大的那个作为控件的宽度
            width = Math.max(desired,size);
        }else {
            if (mode == MeasureSpec.AT_MOST) {
                //如果是wrap_parent,则选取屏幕宽度和计算得到的宽度中较小的那个作为控件的宽度
                width = Math.min(desired,size);
            }else {
                //还有一种情况是 UNSPECIFIED ,一般是系统内部测量的过程中使用,这种情况也可以忽略掉
                width = desired;
            }
        }

        mWidth = width;
        return width;
    }

    /**
     * 测量高
     * @param heightMeasureSpec
     * @return
     */
    private int measureHeight(int heightMeasureSpec) {
        final int mode = MeasureSpec.getMode(heightMeasureSpec);
        final int size = MeasureSpec.getSize(heightMeasureSpec);

        int height;
        int desired = getPaddingTop() + mIndicatorSize + getPaddingBottom();

        if (mode == MeasureSpec.EXACTLY) {
            height = size;
        }else {
            if (mode == MeasureSpec.AT_MOST) {
                height = Math.min(desired,size);
            }else {
                height = desired;
            }
        }

        return height;
    }

    @Override
    protected void onDraw(Canvas canvas) {

        canvas.save();
        /**
         * 这里是计算绘制控件的x轴上的起点位置
         * 当wrap_content并且控件宽度小于屏幕宽度时,计算得到的宽度和实际宽度相同,mWidth/2 - mContextWidth/2 = 0,这种情况很好理解
         * 再看当match_content的情况,控件的计算宽度小于屏幕宽度,则mWidth > mContextWidth,则计算得到的left是计算得到的控件距离屏幕左端的距离,
         * 控件将绘制在屏幕水平方向的中间,不清楚可以在纸上画下,其他情况也类似
         */
        int left = mWidth/2 - mContextWidth/2 + getPaddingLeft();
        canvas.translate(left,getPaddingTop());
        //依次绘制指示器
        for (int i = 0 ; i < mCount ; i++) {
            mIndicator.setState(EMPTY_STATE_SET);
            mIndicator.draw(canvas);
            canvas.translate(mIndicatorSize + mMargin,0);
        }
        canvas.restore();

        /**
         * 下面是绘制选中的指示器的样式
         */
        float leftDraw = ( mIndicatorSize + mMargin ) * ( mSelectedItem + mOffset );
        canvas.translate(left,getPaddingTop());
        canvas.translate(leftDraw,0);
        mIndicator.setState(SELECTED_STATE_SET);
        mIndicator.draw(canvas);

    }

    public void setViewPager(ViewPager viewPager) {
        if (viewPager == null) {
            return;
        }
        PagerAdapter pagerAdapter = viewPager.getAdapter();
        if (pagerAdapter == null) {
            throw new RuntimeException("请先设置viewpager的adaper");
        }
        mCount = pagerAdapter.getCount();
        viewPager.addOnPageChangeListener(this);
        mSelectedItem = viewPager.getCurrentItem();
        invalidate();
    }

    /**
     * 因为自定义控件注册了viewpager的滑动监听,
     * 为了外部能够同样监听到滑动事件,所以提供该接口
     * @param pageChangeListener
     */
    public void setOnPageChangeListener(ViewPager.OnPageChangeListener pageChangeListener) {
        this.mPageChangeListener = pageChangeListener;
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if (mSmooth){
            //允许显示滑动效果
            mSelectedItem = position;
            mOffset = positionOffset;
            //通知view重绘
            invalidate();
        }
        if (mPageChangeListener != null) {
            mPageChangeListener.onPageScrolled(position,positionOffset,positionOffsetPixels);
        }
    }

    @Override
    public void onPageSelected(int position) {
        //当有page被选中后通知view重绘
        mSelectedItem = position ;
        invalidate();

        if(mPageChangeListener != null){
            mPageChangeListener.onPageSelected(position);
        }
    }

    @Override
    public void onPageScrollStateChanged(int state) {
        if(mPageChangeListener != null){
            mPageChangeListener.onPageScrollStateChanged(state);
        }
    }
}

 

使用方式

這時候就可以在activity_main.xml內使用Indicator
 <com.hello.kaiser.viewpager.IndicatorView
            android:id="@+id/id_view_indicator"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            android:layout_gravity="center_horizontal|bottom"
            android:layout_marginBottom="32dp"
            app:indicatorSmooth="false"
            custom:indicatorIcon="@drawable/indicator_selector"
            custom:indicatorMargin="5dp"
            custom:indicatorSize="9dp" />
 
這裡要注意的事,如果你是用複製貼上的方式在看這篇BLOG
你會發現會報錯,因為你寫的IndicatorView位置和我的並不一樣
我寫在com.hello.kaiser.viewpager.IndicatorView內,你必須指定你自己的IndicatorView才有辦法使用

在MainActivity.java內,設定IndicatorView的ViewPager吧
 IndicatorView indicatorView = (IndicatorView) findViewById(R.id.id_view_indicator);
 indicatorView.setViewPager(mViewPager);
 



DEMO
https://github.com/nikeru8/ViewPagerDemo/tree/AddIndicator




Android元件(ViewPager)– 簡單ViewPager頁面 banner自動擴充實現

前言

ViewPagerRecyclerView大概是開發者最常用到的兩個元件了。
最近會圍繞著這兩個主題講一下Adapter的設計。

其實之前講過一篇,其實以前的網誌提過。

看看以前的網誌,才能深刻的感受到原來成長就是這麼一回事XD
今天來講一下ViewPager實現。

你塞給他多少圖片,他會自行擴充的ViewPager

今日實作ViewPager



 重點程式碼

ViewPager和RecyclerView的共同點,重點都是Adapter
 //製作Adapter
    class BannerAdapter extends PagerAdapter {

        private LayoutInflater mInflater;
        private List<String> mUrlList;

        //建構子
        public BannerAdapter(Context context, List<String> mUrlList) {
            //如果把這個寫在instantiateItem的話,每一個Item都會呼叫一次,很吃資源
            mInflater = LayoutInflater.from(context);
            this.mUrlList = mUrlList;
        }

        //看你這ViewPager要有幾頁,靠著List的大小擴充
        @Override
        public int getCount() {
            //如果陣列為空,返回0 防呆機制
            return mUrlList == null ? 0 : mUrlList.size();
        }

        //用來判斷目前的畫面是否和instantiateItem創建的為同一個
        @Override
        public boolean isViewFromObject(View view, Object o) {
            return o == view;
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            //每一個Item都創建View
            View view = mInflater.inflate(R.layout.main_viewpager, container, false);
            ImageView imageView = (ImageView) view.findViewById(R.id.image);

            //用Glide下載圖片
            Glide.with(mContext)
                    .load(mUrlList.get(position))
                    .error(R.drawable.image_loading)
                    .placeholder(R.drawable.image_loading)
                    .centerCrop()
                    .fitCenter()
                    .diskCacheStrategy(DiskCacheStrategy.RESULT)
                    .into(imageView);
            container.addView(view);
            return view;
        }

        //移除ViewPager內所對應的視圖
        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View) object);
        }
    }
 
這邊是我所寫的Adapter
其中下載圖片的方式請參考 Glide 

重點複寫override四個方法

getCount   要顯示的頁數數量

isViewFromObject  判斷目前的畫面是否和instantiateItem創建的為同一個


instantiateItem  Item畫面的製作 有點像RecyclerView內的 onCreateViewHolder + onBindViewHolder


destroyItem  移除ViewPager內所對應的視圖



全部程式碼


今日會碰觸到的地方

drawable內的 image_loading 請自行去找一張你愛的loading圖,網路上很多可自己google或是看下方demo從那邊下載


先在AndroidManifest.xml內配置網路權限
 
<uses-permission android:name="android.permission.INTERNET">
 

然後再build.gradle內配置好下載圖片的Glide
compile 'com.github.bumptech.glide:glide:3.7.0'
 
設定好了讓我們做正事吧!

先來畫
activity_main.xml
activity_main.xml
 
<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.hello.kaiser.viewpager.MainActivity">

    <android.support.v4.view.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:layout_centerInParent="true"
        android:background="@android:color/darker_gray" />

</RelativeLayout>

簡單的塞了一個ViewPager

再來我們的每一個頁面ViewPager需要顯示一張圖片
main_viewpager.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true"
        android:scaleType="fitXY" />
</RelativeLayout>
 

MainActivity.java
public class MainActivity extends AppCompatActivity {

    private Context mContext;

    //參數
    private ViewPager mViewPager;

    //假資料

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = this;
        mViewPager = (ViewPager) findViewById(R.id.view_pager);

        //假資料
        List<String> mUrlList = new ArrayList<>();
        mUrlList.add("http://android.suvenconsultants.com/newimage/android-developer2.png");
        mUrlList.add("http://cdn.makeuseof.com/wp-content/uploads/2017/05/android-apps-eat-battery-670x335.jpg");
        mUrlList.add("https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSpSWJ32XK7TcLUT2XpXTrzwHMOiKHJkzD7QodGrhRzfvRn35Z4mg");
        mUrlList.add("http://android.suvenconsultants.com/newimage/android-developer2.png");

        //呼叫Adapter
        BannerAdapter bannerAdapter = new BannerAdapter(this, mUrlList);
        mViewPager.setAdapter(bannerAdapter);

    }


    //製作Adapter
    class BannerAdapter extends PagerAdapter {

        private LayoutInflater mInflater;
        private List<String> mUrlList;

        //建構子
        public BannerAdapter(Context context, List<String> mUrlList) {
            //如果把這個寫在instantiateItem的話,每一個Item都會呼叫一次,很吃資源
            mInflater = LayoutInflater.from(context);
            this.mUrlList = mUrlList;
        }

        //看你這ViewPager要有幾頁,靠著List的大小擴充
        @Override
        public int getCount() {
            //如果陣列為空,返回0 防呆機制
            return mUrlList == null ? 0 : mUrlList.size();
        }

        //用來判斷目前的畫面是否和instantiateItem創建的為同一個
        @Override
        public boolean isViewFromObject(View view, Object o) {
            return o == view;
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            //每一個Item都創建View
            View view = mInflater.inflate(R.layout.main_viewpager, container, false);
            ImageView imageView = (ImageView) view.findViewById(R.id.image);

            //用Glide下載圖片
            Glide.with(mContext)
                    .load(mUrlList.get(position))
                    .error(R.drawable.image_loading)
                    .placeholder(R.drawable.image_loading)
                    .centerCrop()
                    .fitCenter()
                    .diskCacheStrategy(DiskCacheStrategy.RESULT)
                    .into(imageView);
            container.addView(view);
            return view;
        }

        //移除ViewPager內所對應的視圖
        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View) object);
        }
    }

}

 


需要注意的幾個地方


飄移
在寫demo時,發現我的viewPager會漂移...

檢查了一下發現我犯了幾個錯誤, 在main_viewpager.xml內的ImageView的圖片必須是固定大小。

在做專案時,必須先和美工溝通好,出圖必須一樣大小
或是在這邊把圖片的大小(width 和 height)先行寫死

<ImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true"
        android:scaleType="fitXY" />

android:layout_centerInParent="true" 必須有這個固定住ImageView在中間
ViewPager通常都是固定高度的,如果你需要隨著app的大小做高度伸展,可以使用code自行修改,這邊不再贅述




DEMO
https://github.com/nikeru8/ViewPagerDemo/tree/master


協程(coroutine) - 協程為什麼要學它?

 Coroutine 協程 再強調一次 協程就是由kotlin官方所提供的線程api //Thread Thread { }.start() //Executor val execu...